dailyjs 0.17 bump and receiveSettings methods
This commit is contained in:
parent
34368d3edf
commit
4e54af6768
|
|
@ -2,7 +2,6 @@ import React, { useState, useMemo, useEffect, useRef } from 'react';
|
||||||
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 usePreferredLayerByCount from '@dailyjs/shared/hooks/usePreferredLayerByCount';
|
|
||||||
import { useDeepCompareMemo } from 'use-deep-compare';
|
import { useDeepCompareMemo } from 'use-deep-compare';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -19,7 +18,7 @@ import { useDeepCompareMemo } from 'use-deep-compare';
|
||||||
export const VideoGrid = React.memo(
|
export const VideoGrid = React.memo(
|
||||||
() => {
|
() => {
|
||||||
const containerRef = useRef();
|
const containerRef = useRef();
|
||||||
const { participants, allParticipants } = useParticipants();
|
const { participants } = useParticipants();
|
||||||
const [dimensions, setDimensions] = useState({
|
const [dimensions, setDimensions] = useState({
|
||||||
width: 1,
|
width: 1,
|
||||||
height: 1,
|
height: 1,
|
||||||
|
|
@ -104,10 +103,6 @@ export const VideoGrid = React.memo(
|
||||||
[layout, participants]
|
[layout, participants]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Optimise performance by reducing video quality
|
|
||||||
// when more participants join (if in SFU mode)
|
|
||||||
usePreferredLayerByCount(allParticipants);
|
|
||||||
|
|
||||||
if (!participants.length) {
|
if (!participants.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,11 @@ import React, {
|
||||||
import { Button } from '@dailyjs/shared/components/Button';
|
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 { useCallState } from '@dailyjs/shared/contexts/CallProvider';
|
||||||
import { useParticipants } from '@dailyjs/shared/contexts/ParticipantsProvider';
|
import { useParticipants } from '@dailyjs/shared/contexts/ParticipantsProvider';
|
||||||
|
import { isLocalId } from '@dailyjs/shared/contexts/participantsState';
|
||||||
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 usePreferredLayerByCount from '@dailyjs/shared/hooks/usePreferredLayerByCount';
|
|
||||||
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 { useDeepCompareMemo } from 'use-deep-compare';
|
import { useDeepCompareMemo } from 'use-deep-compare';
|
||||||
|
|
@ -21,6 +22,7 @@ const MIN_TILE_WIDTH = 280;
|
||||||
const MAX_TILES_PER_PAGE = 12;
|
const MAX_TILES_PER_PAGE = 12;
|
||||||
|
|
||||||
export const PaginatedVideoGrid = () => {
|
export const PaginatedVideoGrid = () => {
|
||||||
|
const { callObject } = useCallState();
|
||||||
const {
|
const {
|
||||||
activeParticipant,
|
activeParticipant,
|
||||||
participantCount,
|
participantCount,
|
||||||
|
|
@ -46,6 +48,8 @@ export const PaginatedVideoGrid = () => {
|
||||||
|
|
||||||
const gridRef = useRef(null);
|
const gridRef = useRef(null);
|
||||||
|
|
||||||
|
// -- Layout / UI
|
||||||
|
|
||||||
// Update width and height of grid when window is resized
|
// Update width and height of grid when window is resized
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let frame;
|
let frame;
|
||||||
|
|
@ -131,26 +135,27 @@ export const PaginatedVideoGrid = () => {
|
||||||
[page, pageSize, participants]
|
[page, pageSize, participants]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// -- Track subscriptions
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Play / pause tracks based on pagination
|
* Play / pause tracks based on pagination
|
||||||
* Note: we pause adjacent page tracks and unsubscribe from everything else
|
* Note: we pause adjacent page tracks and unsubscribe from everything else
|
||||||
* Please refer to project README for more information
|
|
||||||
*/
|
*/
|
||||||
const camSubscriptions = useMemo(() => {
|
const camSubscriptions = useMemo(() => {
|
||||||
const maxSubs = 3 * pageSize;
|
const maxSubs = 3 * pageSize;
|
||||||
|
|
||||||
// Determine participant ids to subscribe to, based on page.
|
// Determine participant ids to subscribe to or stage, based on page
|
||||||
let subscribedIds = [];
|
let renderedOrBufferedIds = [];
|
||||||
switch (page) {
|
switch (page) {
|
||||||
// First page
|
// First page
|
||||||
case 1:
|
case 1:
|
||||||
subscribedIds = participants
|
renderedOrBufferedIds = participants
|
||||||
.slice(0, Math.min(maxSubs, 2 * pageSize))
|
.slice(0, Math.min(maxSubs, 2 * pageSize))
|
||||||
.map((p) => p.id);
|
.map((p) => p.id);
|
||||||
break;
|
break;
|
||||||
// Last page
|
// Last page
|
||||||
case Math.ceil(participants.length / pageSize):
|
case Math.ceil(participants.length / pageSize):
|
||||||
subscribedIds = participants
|
renderedOrBufferedIds = participants
|
||||||
.slice(-Math.min(maxSubs, 2 * pageSize))
|
.slice(-Math.min(maxSubs, 2 * pageSize))
|
||||||
.map((p) => p.id);
|
.map((p) => p.id);
|
||||||
break;
|
break;
|
||||||
|
|
@ -160,18 +165,29 @@ export const PaginatedVideoGrid = () => {
|
||||||
const buffer = (maxSubs - pageSize) / 2;
|
const buffer = (maxSubs - pageSize) / 2;
|
||||||
const min = (page - 1) * pageSize - buffer;
|
const min = (page - 1) * pageSize - buffer;
|
||||||
const max = page * pageSize + buffer;
|
const max = page * pageSize + buffer;
|
||||||
subscribedIds = participants.slice(min, max).map((p) => p.id);
|
renderedOrBufferedIds = participants.slice(min, max).map((p) => p.id);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine subscribed, but invisible (= paused) video tracks
|
const subscribedIds = [];
|
||||||
const invisibleSubscribedIds = subscribedIds.filter(
|
const stagedIds = [];
|
||||||
(id) => id !== 'local' && !visibleParticipants.some((vp) => vp.id === id)
|
|
||||||
);
|
// Decide whether to subscribe to or stage participants'
|
||||||
|
// track based on isibility
|
||||||
|
renderedOrBufferedIds.forEach((id) => {
|
||||||
|
if (id !== isLocalId()) {
|
||||||
|
if (visibleParticipants.some((vp) => vp.id === id)) {
|
||||||
|
subscribedIds.push(id);
|
||||||
|
} else {
|
||||||
|
stagedIds.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subscribedIds: subscribedIds.filter((id) => id !== 'local'),
|
subscribedIds,
|
||||||
pausedIds: invisibleSubscribedIds,
|
stagedIds,
|
||||||
};
|
};
|
||||||
}, [page, pageSize, participants, visibleParticipants]);
|
}, [page, pageSize, participants, visibleParticipants]);
|
||||||
|
|
||||||
|
|
@ -180,8 +196,36 @@ export const PaginatedVideoGrid = () => {
|
||||||
camSubscriptions?.pausedIds
|
camSubscriptions?.pausedIds
|
||||||
);
|
);
|
||||||
|
|
||||||
// Set bandwidth layer based on amount of visible participants
|
/**
|
||||||
usePreferredLayerByCount(visibleParticipants);
|
* Set bandwidth layer based on amount of visible participants
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (!(callObject && callObject.meetingState() === 'joined-meeting')) return;
|
||||||
|
const count = visibleParticipants.length;
|
||||||
|
|
||||||
|
let layer;
|
||||||
|
if (count < 5) {
|
||||||
|
// highest quality layer
|
||||||
|
layer = 2;
|
||||||
|
} else if (count < 10) {
|
||||||
|
// mid quality layer
|
||||||
|
layer = 1;
|
||||||
|
} else {
|
||||||
|
// low qualtiy layer
|
||||||
|
layer = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const receiveSettings = visibleParticipants.reduce(
|
||||||
|
(settings, participant) => {
|
||||||
|
if (isLocalId(participant.id)) return settings;
|
||||||
|
return { ...settings, [participant.id]: { video: { layer } } };
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
callObject.updateReceiveSettings(receiveSettings);
|
||||||
|
}, [visibleParticipants, callObject]);
|
||||||
|
|
||||||
|
// -- Active speaker
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle position updates based on active speaker events
|
* Handle position updates based on active speaker events
|
||||||
|
|
@ -255,7 +299,9 @@ export const PaginatedVideoGrid = () => {
|
||||||
>
|
>
|
||||||
<IconArrow />
|
<IconArrow />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div className="tiles">{tiles}</div>
|
<div className="tiles">{tiles}</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
className="page-button next"
|
className="page-button next"
|
||||||
disabled={!(pages > 1 && page < pages)}
|
disabled={!(pages > 1 && page < pages)}
|
||||||
|
|
@ -264,6 +310,7 @@ export const PaginatedVideoGrid = () => {
|
||||||
>
|
>
|
||||||
<IconArrow />
|
<IconArrow />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<style jsx>{`
|
<style jsx>{`
|
||||||
.grid {
|
.grid {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
|
|
@ -34,16 +34,23 @@ const CombinedAudioTrack = ({ tracks }) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
audio.load();
|
const playAudio = async () => {
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
stream
|
||||||
|
.getAudioTracks()
|
||||||
|
.some((t) => t.enabled && t.readyState === 'live') &&
|
||||||
|
audio.paused
|
||||||
|
) {
|
||||||
|
await audio.play();
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (
|
audio.load();
|
||||||
stream
|
playAudio();
|
||||||
.getAudioTracks()
|
|
||||||
.some((t) => t.enabled && t.readyState === 'live') &&
|
|
||||||
audio.paused
|
|
||||||
) {
|
|
||||||
audio.play();
|
|
||||||
}
|
|
||||||
}, [tracks, trackIds]);
|
}, [tracks, trackIds]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -52,10 +52,6 @@ export const BasicTray = () => {
|
||||||
<IconPeople />
|
<IconPeople />
|
||||||
</TrayButton>
|
</TrayButton>
|
||||||
|
|
||||||
<TrayButton label="Fake" onClick={() => callObject.addFakeParticipant()}>
|
|
||||||
+
|
|
||||||
</TrayButton>
|
|
||||||
|
|
||||||
{customTrayComponent}
|
{customTrayComponent}
|
||||||
|
|
||||||
<span className="divider" />
|
<span className="divider" />
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import {
|
||||||
ACCESS_STATE_UNKNOWN,
|
ACCESS_STATE_UNKNOWN,
|
||||||
VIDEO_QUALITY_AUTO,
|
VIDEO_QUALITY_AUTO,
|
||||||
} from '../constants';
|
} from '../constants';
|
||||||
|
import { useNetworkState } from '../hooks/useNetworkState';
|
||||||
import { useCallMachine } from './useCallMachine';
|
import { useCallMachine } from './useCallMachine';
|
||||||
|
|
||||||
export const CallContext = createContext();
|
export const CallContext = createContext();
|
||||||
|
|
@ -44,6 +45,7 @@ export const CallProvider = ({
|
||||||
token,
|
token,
|
||||||
subscribeToTracksAutomatically,
|
subscribeToTracksAutomatically,
|
||||||
});
|
});
|
||||||
|
const networkState = useNetworkState(daily, videoQuality);
|
||||||
|
|
||||||
// Feature detection taken from daily room object and client browser support
|
// Feature detection taken from daily room object and client browser support
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -107,6 +109,7 @@ export const CallProvider = ({
|
||||||
addFakeParticipant,
|
addFakeParticipant,
|
||||||
preJoinNonAuthorized,
|
preJoinNonAuthorized,
|
||||||
leave,
|
leave,
|
||||||
|
networkState,
|
||||||
showLocalVideo,
|
showLocalVideo,
|
||||||
roomExp,
|
roomExp,
|
||||||
videoQuality,
|
videoQuality,
|
||||||
|
|
@ -115,8 +118,10 @@ export const CallProvider = ({
|
||||||
setBandwidth,
|
setBandwidth,
|
||||||
setRedirectOnLeave,
|
setRedirectOnLeave,
|
||||||
setShowLocalVideo,
|
setShowLocalVideo,
|
||||||
|
setVideoQuality,
|
||||||
startCloudRecording,
|
startCloudRecording,
|
||||||
subscribeToTracksAutomatically,
|
subscribeToTracksAutomatically,
|
||||||
|
videoQuality,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,12 @@ import {
|
||||||
} from '@dailyjs/shared/contexts/UIStateProvider';
|
} from '@dailyjs/shared/contexts/UIStateProvider';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import {
|
||||||
|
VIDEO_QUALITY_AUTO,
|
||||||
|
VIDEO_QUALITY_BANDWIDTH_SAVER,
|
||||||
|
VIDEO_QUALITY_LOW,
|
||||||
|
VIDEO_QUALITY_VERY_LOW,
|
||||||
|
} from '../constants';
|
||||||
import { sortByKey } from '../lib/sortByKey';
|
import { sortByKey } from '../lib/sortByKey';
|
||||||
|
|
||||||
import { useCallState } from './CallProvider';
|
import { useCallState } from './CallProvider';
|
||||||
|
|
@ -33,7 +39,7 @@ import {
|
||||||
export const ParticipantsContext = createContext();
|
export const ParticipantsContext = createContext();
|
||||||
|
|
||||||
export const ParticipantsProvider = ({ children }) => {
|
export const ParticipantsProvider = ({ children }) => {
|
||||||
const { callObject } = useCallState();
|
const { callObject, videoQuality, networkState } = useCallState();
|
||||||
const [state, dispatch] = useReducer(
|
const [state, dispatch] = useReducer(
|
||||||
participantsReducer,
|
participantsReducer,
|
||||||
initialParticipantsState
|
initialParticipantsState
|
||||||
|
|
@ -235,34 +241,50 @@ export const ParticipantsProvider = ({ children }) => {
|
||||||
}, [callObject, handleNewParticipantsState]);
|
}, [callObject, handleNewParticipantsState]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adjust video quality from the 3 simulcast layers based
|
* Change between the simulcast layers based on view / available bandwidth
|
||||||
* on active speaker status. Note: this currently uses
|
|
||||||
* undocumented internal methods (we'll be adding support
|
|
||||||
* for this into our API soon!)
|
|
||||||
*/
|
*/
|
||||||
const setBandWidthControls = useCallback(() => {
|
const setBandWidthControls = useCallback(() => {
|
||||||
if (typeof rtcpeers === 'undefined') return;
|
if (!(callObject && callObject.meetingState() === 'joined-meeting')) return;
|
||||||
const sfu = rtcpeers?.soup;
|
|
||||||
const isSFU = rtcpeers?.currentlyPreferred?.typeName?.() === 'sfu';
|
|
||||||
if (!isSFU) return;
|
|
||||||
|
|
||||||
const ids = participantIds.split(',');
|
const ids = participantIds.split(',');
|
||||||
|
const receiveSettings = {};
|
||||||
|
|
||||||
ids.forEach((id) => {
|
ids.forEach((id) => {
|
||||||
if (isLocalId(id)) return;
|
if (isLocalId(id)) return;
|
||||||
|
|
||||||
|
if (
|
||||||
|
// weak or bad network
|
||||||
|
([VIDEO_QUALITY_LOW, VIDEO_QUALITY_VERY_LOW].includes(networkState) &&
|
||||||
|
videoQuality === VIDEO_QUALITY_AUTO) ||
|
||||||
|
// Low quality or Bandwidth saver mode enabled
|
||||||
|
[VIDEO_QUALITY_BANDWIDTH_SAVER, VIDEO_QUALITY_LOW].includes(
|
||||||
|
videoQuality
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
receiveSettings[id] = { video: { layer: 0 } };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Speaker view settings based on speaker status or pinned user
|
// Speaker view settings based on speaker status or pinned user
|
||||||
if (viewMode === VIEW_MODE_SPEAKER) {
|
if (viewMode === VIEW_MODE_SPEAKER) {
|
||||||
if (currentSpeaker?.id === id) {
|
if (currentSpeaker?.id === id) {
|
||||||
sfu.setPreferredLayerForTrack(id, 'cam-video', 2);
|
receiveSettings[id] = { video: { layer: 2 } };
|
||||||
} else {
|
} else {
|
||||||
sfu.setPreferredLayerForTrack(id, 'cam-video', 0);
|
receiveSettings[id] = { video: { layer: 0 } };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: grid view settings are handled by the grid view component
|
// Grid view settings are handled separately in GridView
|
||||||
});
|
});
|
||||||
}, [currentSpeaker?.id, participantIds, viewMode]);
|
callObject.updateReceiveSettings(receiveSettings);
|
||||||
|
}, [
|
||||||
|
currentSpeaker?.id,
|
||||||
|
callObject,
|
||||||
|
networkState,
|
||||||
|
participantIds,
|
||||||
|
videoQuality,
|
||||||
|
viewMode,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setBandWidthControls();
|
setBandWidthControls();
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ const MAX_RECENT_SPEAKER_COUNT = 6;
|
||||||
* If the remote participant count passes this threshold,
|
* If the remote participant count passes this threshold,
|
||||||
* cam subscriptions are defined by UI view modes.
|
* cam subscriptions are defined by UI view modes.
|
||||||
*/
|
*/
|
||||||
const SUBSCRIBE_ALL_VIDEO_THRESHOLD = 9;
|
const SUBSCRIBE_OR_STAGE_ALL_VIDEO_THRESHOLD = 9;
|
||||||
|
|
||||||
const TracksContext = createContext(null);
|
const TracksContext = createContext(null);
|
||||||
|
|
||||||
|
|
@ -52,135 +52,81 @@ export const TracksProvider = ({ children }) => {
|
||||||
[participants]
|
[participants]
|
||||||
);
|
);
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
rtcpeers.soup.pauseTrack(id, 'cam-video');
|
|
||||||
},
|
|
||||||
[subscribeToTracksAutomatically]
|
|
||||||
);
|
|
||||||
|
|
||||||
const resumeVideoTrack = useCallback(
|
|
||||||
(id) => {
|
|
||||||
/**
|
|
||||||
* Ignore undefined, local or screenshare.
|
|
||||||
*/
|
|
||||||
if (
|
|
||||||
!id ||
|
|
||||||
subscribeToTracksAutomatically ||
|
|
||||||
isLocalId(id) ||
|
|
||||||
isScreenId(id)
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
const videoTrack = callObject.participants()?.[id]?.tracks?.video;
|
|
||||||
|
|
||||||
const subscribe = () => {
|
|
||||||
if (videoTrack?.subscribed) return;
|
|
||||||
callObject.updateParticipant(id, {
|
|
||||||
setSubscribedTracks: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
switch (rtcpeers.getCurrentType()) {
|
|
||||||
case 'peer-to-peer':
|
|
||||||
subscribe();
|
|
||||||
break;
|
|
||||||
case 'sfu': {
|
|
||||||
if (!rtcpeers.soup.implementationIsAcceptingCalls) return;
|
|
||||||
rtcpeers.soup.resumeTrack(id, 'cam-video');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[callObject, subscribeToTracksAutomatically]
|
|
||||||
);
|
|
||||||
|
|
||||||
const remoteParticipantIds = useMemo(
|
const remoteParticipantIds = useMemo(
|
||||||
() => participants.filter((p) => !p.isLocal).map((p) => p.id),
|
() => participants.filter((p) => !p.isLocal).map((p) => p.id),
|
||||||
[participants]
|
[participants]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const subscribeToCam = useCallback(
|
||||||
|
(id) => {
|
||||||
|
// Ignore undefined, local or screenshare.
|
||||||
|
if (!id || isLocalId(id) || isScreenId(id)) return;
|
||||||
|
callObject.updateParticipant(id, {
|
||||||
|
setSubscribedTracks: { video: true },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[callObject]
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates cam subscriptions based on passed ids.
|
* Updates cam subscriptions based on passed subscribedIds and stagedIds.
|
||||||
*
|
* For ids not provided, cam tracks will be unsubscribed from
|
||||||
* @param ids Array of ids to subscribe to, all others will be unsubscribed.
|
|
||||||
* @param pausedIds Array of ids that should be subscribed, but paused.
|
|
||||||
*/
|
*/
|
||||||
const updateCamSubscriptions = useCallback(
|
const updateCamSubscriptions = useCallback(
|
||||||
(ids, pausedIds = []) => {
|
(subscribedIds, stagedIds = []) => {
|
||||||
if (!callObject || subscribeToTracksAutomatically) return;
|
if (!callObject) return;
|
||||||
const subscribedIds =
|
|
||||||
remoteParticipantIds.length <= SUBSCRIBE_ALL_VIDEO_THRESHOLD
|
|
||||||
? [...remoteParticipantIds]
|
|
||||||
: [...ids, ...recentSpeakerIds];
|
|
||||||
|
|
||||||
|
// If total number of remote participants is less than a threshold, simply
|
||||||
|
// stage all remote cams that aren't already marked for subscription.
|
||||||
|
// Otherwise, honor the provided stagedIds, with recent speakers appended
|
||||||
|
// who aren't already marked for subscription.
|
||||||
|
const stagedIdsFiltered =
|
||||||
|
remoteParticipantIds.length <= SUBSCRIBE_OR_STAGE_ALL_VIDEO_THRESHOLD
|
||||||
|
? remoteParticipantIds.filter((id) => !subscribedIds.includes(id))
|
||||||
|
: [
|
||||||
|
...stagedIds,
|
||||||
|
...recentSpeakerIds.filter((id) => !subscribedIds.includes(id)),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Assemble updates to get to desired cam subscriptions
|
||||||
const updates = remoteParticipantIds.reduce((u, id) => {
|
const updates = remoteParticipantIds.reduce((u, id) => {
|
||||||
const shouldSubscribe = subscribedIds.includes(id);
|
let desiredSubscription;
|
||||||
const shouldPause = pausedIds.includes(id);
|
const currentSubscription =
|
||||||
const isSubscribed =
|
|
||||||
callObject.participants()?.[id]?.tracks?.video?.subscribed;
|
callObject.participants()?.[id]?.tracks?.video?.subscribed;
|
||||||
|
|
||||||
/**
|
// Ignore undefined, local or screenshare participant ids
|
||||||
* Pause already subscribed tracks.
|
if (!id || isLocalId(id) || isScreenId(id)) return u;
|
||||||
*/
|
|
||||||
if (shouldSubscribe && shouldPause) {
|
// Decide on desired cam subscription for this participant:
|
||||||
pauseVideoTrack(id);
|
// subscribed, staged, or unsubscribed
|
||||||
|
if (subscribedIds.includes(id)) {
|
||||||
|
desiredSubscription = true;
|
||||||
|
} else if (stagedIdsFiltered.includes(id)) {
|
||||||
|
desiredSubscription = 'staged';
|
||||||
|
} else {
|
||||||
|
desiredSubscription = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Skip if we already have the desired subscription to this
|
||||||
* Fast resume tracks.
|
// participant's cam
|
||||||
*/
|
if (desiredSubscription === currentSubscription) return u;
|
||||||
if (shouldSubscribe && !shouldPause) {
|
|
||||||
resumeVideoTrack(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
return {
|
||||||
isLocalId(id) ||
|
...u,
|
||||||
isScreenId(id) ||
|
[id]: {
|
||||||
(shouldSubscribe && isSubscribed)
|
setSubscribedTracks: {
|
||||||
) {
|
audio: true,
|
||||||
return u;
|
screenAudio: true,
|
||||||
}
|
screenVideo: true,
|
||||||
|
video: desiredSubscription,
|
||||||
const result = {
|
},
|
||||||
setSubscribedTracks: {
|
|
||||||
audio: true,
|
|
||||||
screenAudio: true,
|
|
||||||
screenVideo: true,
|
|
||||||
video: shouldSubscribe,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return { ...u, [id]: result };
|
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
callObject.updateParticipants(updates);
|
callObject.updateParticipants(updates);
|
||||||
},
|
},
|
||||||
[
|
[callObject, remoteParticipantIds, recentSpeakerIds]
|
||||||
callObject,
|
|
||||||
subscribeToTracksAutomatically,
|
|
||||||
remoteParticipantIds,
|
|
||||||
recentSpeakerIds,
|
|
||||||
pauseVideoTrack,
|
|
||||||
resumeVideoTrack,
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -273,15 +219,14 @@ 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, subscribeToTracksAutomatically, pauseVideoTrack]);
|
}, [callObject, subscribeToTracksAutomatically]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TracksContext.Provider
|
<TracksContext.Provider
|
||||||
value={{
|
value={{
|
||||||
audioTracks: state.audioTracks,
|
audioTracks: state.audioTracks,
|
||||||
videoTracks: state.videoTracks,
|
videoTracks: state.videoTracks,
|
||||||
pauseVideoTrack,
|
subscribeToCam,
|
||||||
resumeVideoTrack,
|
|
||||||
updateCamSubscriptions,
|
updateCamSubscriptions,
|
||||||
remoteParticipantIds,
|
remoteParticipantIds,
|
||||||
recentSpeakerIds,
|
recentSpeakerIds,
|
||||||
|
|
|
||||||
|
|
@ -2,23 +2,25 @@ import { useDeepCompareEffect } from 'use-deep-compare';
|
||||||
import { useTracks } from '../contexts/TracksProvider';
|
import { useTracks } from '../contexts/TracksProvider';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates cam subscriptions based on passed ids and pausedIds.
|
* Updates cam subscriptions based on passed subscribedIds and stagedIds.
|
||||||
* @param ids Participant ids which should be subscribed to.
|
* @param subscribedIds Participant ids whose cam tracks should be subscribed to.
|
||||||
* @param pausedIds Participant ids which should be subscribed, but paused.
|
* @param stagedIds Participant ids whose cam tracks should be staged.
|
||||||
* @param delay Throttle in milliseconds. Default: 50
|
* @param delay Throttle in milliseconds. Default: 50
|
||||||
*/
|
*/
|
||||||
export const useCamSubscriptions = (ids, pausedIds = [], throttle = 50) => {
|
export const useCamSubscriptions = (
|
||||||
|
subscribedIds,
|
||||||
|
stagedIds,
|
||||||
|
throttle = 50
|
||||||
|
) => {
|
||||||
const { updateCamSubscriptions } = useTracks();
|
const { updateCamSubscriptions } = useTracks();
|
||||||
|
|
||||||
useDeepCompareEffect(() => {
|
useDeepCompareEffect(() => {
|
||||||
if (!ids || !pausedIds) return false;
|
if (!subscribedIds || !stagedIds) return false;
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
updateCamSubscriptions(ids, pausedIds);
|
updateCamSubscriptions(subscribedIds, stagedIds);
|
||||||
}, throttle);
|
}, throttle);
|
||||||
return () => {
|
return () => clearTimeout(timeout);
|
||||||
clearTimeout(timeout);
|
}, [subscribedIds, stagedIds, throttle, updateCamSubscriptions]);
|
||||||
};
|
|
||||||
}, [ids, pausedIds, throttle, updateCamSubscriptions]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useCamSubscriptions;
|
export default useCamSubscriptions;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
/* global rtcpeers */
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
VIDEO_QUALITY_HIGH,
|
||||||
|
VIDEO_QUALITY_LOW,
|
||||||
|
VIDEO_QUALITY_BANDWIDTH_SAVER,
|
||||||
|
} from '../constants';
|
||||||
|
|
||||||
|
export const NETWORK_STATE_GOOD = 'good';
|
||||||
|
export const NETWORK_STATE_LOW = 'low';
|
||||||
|
export const NETWORK_STATE_VERY_LOW = 'very-low';
|
||||||
|
const STANDARD_HIGH_BITRATE_CAP = 980;
|
||||||
|
const STANDARD_LOW_BITRATE_CAP = 300;
|
||||||
|
|
||||||
|
export const useNetworkState = (
|
||||||
|
callObject = null,
|
||||||
|
quality = VIDEO_QUALITY_HIGH
|
||||||
|
) => {
|
||||||
|
const [threshold, setThreshold] = useState(NETWORK_STATE_GOOD);
|
||||||
|
|
||||||
|
const setQuality = useCallback(
|
||||||
|
(q) => {
|
||||||
|
if (!callObject || typeof rtcpeers === 'undefined') return;
|
||||||
|
|
||||||
|
const peers = Object.keys(callObject.participants()).length - 1;
|
||||||
|
const isSFU = rtcpeers?.currentlyPreferred?.typeName?.() === 'sfu';
|
||||||
|
|
||||||
|
const lowKbs = isSFU
|
||||||
|
? STANDARD_LOW_BITRATE_CAP
|
||||||
|
: STANDARD_LOW_BITRATE_CAP / Math.max(1, peers);
|
||||||
|
|
||||||
|
switch (q) {
|
||||||
|
case VIDEO_QUALITY_HIGH:
|
||||||
|
callObject.setBandwidth({ kbs: STANDARD_HIGH_BITRATE_CAP });
|
||||||
|
break;
|
||||||
|
case VIDEO_QUALITY_LOW:
|
||||||
|
callObject.setBandwidth({
|
||||||
|
kbs: lowKbs,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case VIDEO_QUALITY_BANDWIDTH_SAVER:
|
||||||
|
callObject.setLocalVideo(false);
|
||||||
|
callObject.setBandwidth({
|
||||||
|
kbs: lowKbs,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[callObject]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleNetworkQualityChange = useCallback(
|
||||||
|
(ev) => {
|
||||||
|
if (ev.threshold === threshold) return;
|
||||||
|
|
||||||
|
switch (ev.threshold) {
|
||||||
|
case NETWORK_STATE_VERY_LOW:
|
||||||
|
setQuality(VIDEO_QUALITY_BANDWIDTH_SAVER);
|
||||||
|
setThreshold(NETWORK_STATE_VERY_LOW);
|
||||||
|
break;
|
||||||
|
case NETWORK_STATE_LOW:
|
||||||
|
setQuality(
|
||||||
|
quality === VIDEO_QUALITY_BANDWIDTH_SAVER
|
||||||
|
? quality
|
||||||
|
: NETWORK_STATE_LOW
|
||||||
|
);
|
||||||
|
setThreshold(NETWORK_STATE_LOW);
|
||||||
|
break;
|
||||||
|
case NETWORK_STATE_GOOD:
|
||||||
|
setQuality(
|
||||||
|
[VIDEO_QUALITY_BANDWIDTH_SAVER, VIDEO_QUALITY_LOW].includes(quality)
|
||||||
|
? quality
|
||||||
|
: VIDEO_QUALITY_HIGH
|
||||||
|
);
|
||||||
|
setThreshold(NETWORK_STATE_GOOD);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setQuality, threshold, quality]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!callObject) return false;
|
||||||
|
callObject.on('network-quality-change', handleNetworkQualityChange);
|
||||||
|
return () =>
|
||||||
|
callObject.off('network-quality-change', handleNetworkQualityChange);
|
||||||
|
}, [callObject, handleNetworkQualityChange]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setQuality(quality);
|
||||||
|
}, [quality, setQuality]);
|
||||||
|
|
||||||
|
return threshold;
|
||||||
|
};
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
/* global rtcpeers */
|
|
||||||
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This hook will switch between one of the 3 simulcast layers
|
|
||||||
* depending on the number of participants present on the call
|
|
||||||
* to optimise bandwidth / cpu usage
|
|
||||||
*
|
|
||||||
* Note: the API for this feature is currently work in progress
|
|
||||||
* and not documented. Momentarily we are using an internal
|
|
||||||
* method `setPreferredLayerForTrack` found on the global
|
|
||||||
* `rtcpeers` object.
|
|
||||||
*
|
|
||||||
* Note: this will have no effect when not in SFU mode
|
|
||||||
*/
|
|
||||||
export const usePreferredLayerByCount = (participants) => {
|
|
||||||
/**
|
|
||||||
* Set bandwidth layer based on amount of visible participants
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
|
||||||
if (typeof rtcpeers === 'undefined' || rtcpeers?.getCurrentType() !== 'sfu')
|
|
||||||
return;
|
|
||||||
|
|
||||||
const sfu = rtcpeers.soup;
|
|
||||||
const count = participants.length;
|
|
||||||
|
|
||||||
participants.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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, [participants]);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default usePreferredLayerByCount;
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@daily-co/daily-js": "^0.15.0",
|
"@daily-co/daily-js": "^0.16.0",
|
||||||
"bowser": "^2.11.0",
|
"bowser": "^2.11.0",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"debounce": "^1.2.1",
|
"debounce": "^1.2.1",
|
||||||
|
|
|
||||||
|
|
@ -160,10 +160,10 @@
|
||||||
"@babel/helper-validator-identifier" "^7.12.11"
|
"@babel/helper-validator-identifier" "^7.12.11"
|
||||||
to-fast-properties "^2.0.0"
|
to-fast-properties "^2.0.0"
|
||||||
|
|
||||||
"@daily-co/daily-js@^0.15.0":
|
"@daily-co/daily-js@^0.16.0":
|
||||||
version "0.15.0"
|
version "0.16.0"
|
||||||
resolved "https://registry.yarnpkg.com/@daily-co/daily-js/-/daily-js-0.15.0.tgz#9dfd5c3ed8855df31c370d5b21a3b5098cce3c4f"
|
resolved "https://registry.yarnpkg.com/@daily-co/daily-js/-/daily-js-0.16.0.tgz#9020104bb88de62dcc1966e713da65844243b9ab"
|
||||||
integrity sha512-rnivho7yx/yEOtqL81L4daPy9C/FDXf06k06df8vmyUXsE8y+cxSTD7ZvYIJDGJHN6IZRhVxxfbCyPI8CHfwCg==
|
integrity sha512-DBWzbZs2IR7uYqfbABva1Ms3f/oX85dnQnCpVbGbexTN63LPIGknFSQp31ZYED88qcG+YJNydywBTb+ApNiNXA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.12.5"
|
"@babel/runtime" "^7.12.5"
|
||||||
bowser "^2.8.1"
|
bowser "^2.8.1"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue