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 { DEFAULT_ASPECT_RATIO } from '@dailyjs/shared/constants';
|
||||
import { useParticipants } from '@dailyjs/shared/contexts/ParticipantsProvider';
|
||||
import usePreferredLayerByCount from '@dailyjs/shared/hooks/usePreferredLayerByCount';
|
||||
import { useDeepCompareMemo } from 'use-deep-compare';
|
||||
|
||||
/**
|
||||
|
|
@ -19,7 +18,7 @@ import { useDeepCompareMemo } from 'use-deep-compare';
|
|||
export const VideoGrid = React.memo(
|
||||
() => {
|
||||
const containerRef = useRef();
|
||||
const { participants, allParticipants } = useParticipants();
|
||||
const { participants } = useParticipants();
|
||||
const [dimensions, setDimensions] = useState({
|
||||
width: 1,
|
||||
height: 1,
|
||||
|
|
@ -104,10 +103,6 @@ export const VideoGrid = React.memo(
|
|||
[layout, participants]
|
||||
);
|
||||
|
||||
// Optimise performance by reducing video quality
|
||||
// when more participants join (if in SFU mode)
|
||||
usePreferredLayerByCount(allParticipants);
|
||||
|
||||
if (!participants.length) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,10 +8,11 @@ import React, {
|
|||
import { Button } from '@dailyjs/shared/components/Button';
|
||||
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 { isLocalId } from '@dailyjs/shared/contexts/participantsState';
|
||||
import { useActiveSpeaker } from '@dailyjs/shared/hooks/useActiveSpeaker';
|
||||
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 sortByKey from '@dailyjs/shared/lib/sortByKey';
|
||||
import { useDeepCompareMemo } from 'use-deep-compare';
|
||||
|
|
@ -21,6 +22,7 @@ const MIN_TILE_WIDTH = 280;
|
|||
const MAX_TILES_PER_PAGE = 12;
|
||||
|
||||
export const PaginatedVideoGrid = () => {
|
||||
const { callObject } = useCallState();
|
||||
const {
|
||||
activeParticipant,
|
||||
participantCount,
|
||||
|
|
@ -46,6 +48,8 @@ export const PaginatedVideoGrid = () => {
|
|||
|
||||
const gridRef = useRef(null);
|
||||
|
||||
// -- Layout / UI
|
||||
|
||||
// Update width and height of grid when window is resized
|
||||
useEffect(() => {
|
||||
let frame;
|
||||
|
|
@ -131,26 +135,27 @@ export const PaginatedVideoGrid = () => {
|
|||
[page, pageSize, participants]
|
||||
);
|
||||
|
||||
// -- Track subscriptions
|
||||
|
||||
/**
|
||||
* Play / pause tracks based on pagination
|
||||
* Note: we pause adjacent page tracks and unsubscribe from everything else
|
||||
* Please refer to project README for more information
|
||||
*/
|
||||
const camSubscriptions = useMemo(() => {
|
||||
const maxSubs = 3 * pageSize;
|
||||
|
||||
// Determine participant ids to subscribe to, based on page.
|
||||
let subscribedIds = [];
|
||||
// Determine participant ids to subscribe to or stage, based on page
|
||||
let renderedOrBufferedIds = [];
|
||||
switch (page) {
|
||||
// First page
|
||||
case 1:
|
||||
subscribedIds = participants
|
||||
renderedOrBufferedIds = participants
|
||||
.slice(0, Math.min(maxSubs, 2 * pageSize))
|
||||
.map((p) => p.id);
|
||||
break;
|
||||
// Last page
|
||||
case Math.ceil(participants.length / pageSize):
|
||||
subscribedIds = participants
|
||||
renderedOrBufferedIds = participants
|
||||
.slice(-Math.min(maxSubs, 2 * pageSize))
|
||||
.map((p) => p.id);
|
||||
break;
|
||||
|
|
@ -160,18 +165,29 @@ export const PaginatedVideoGrid = () => {
|
|||
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);
|
||||
renderedOrBufferedIds = 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)
|
||||
);
|
||||
const subscribedIds = [];
|
||||
const stagedIds = [];
|
||||
|
||||
// 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 {
|
||||
subscribedIds: subscribedIds.filter((id) => id !== 'local'),
|
||||
pausedIds: invisibleSubscribedIds,
|
||||
subscribedIds,
|
||||
stagedIds,
|
||||
};
|
||||
}, [page, pageSize, participants, visibleParticipants]);
|
||||
|
||||
|
|
@ -180,8 +196,36 @@ export const PaginatedVideoGrid = () => {
|
|||
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
|
||||
|
|
@ -255,7 +299,9 @@ export const PaginatedVideoGrid = () => {
|
|||
>
|
||||
<IconArrow />
|
||||
</Button>
|
||||
|
||||
<div className="tiles">{tiles}</div>
|
||||
|
||||
<Button
|
||||
className="page-button next"
|
||||
disabled={!(pages > 1 && page < pages)}
|
||||
|
|
@ -264,6 +310,7 @@ export const PaginatedVideoGrid = () => {
|
|||
>
|
||||
<IconArrow />
|
||||
</Button>
|
||||
|
||||
<style jsx>{`
|
||||
.grid {
|
||||
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 (
|
||||
stream
|
||||
.getAudioTracks()
|
||||
.some((t) => t.enabled && t.readyState === 'live') &&
|
||||
audio.paused
|
||||
) {
|
||||
audio.play();
|
||||
}
|
||||
audio.load();
|
||||
playAudio();
|
||||
}, [tracks, trackIds]);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -52,10 +52,6 @@ export const BasicTray = () => {
|
|||
<IconPeople />
|
||||
</TrayButton>
|
||||
|
||||
<TrayButton label="Fake" onClick={() => callObject.addFakeParticipant()}>
|
||||
+
|
||||
</TrayButton>
|
||||
|
||||
{customTrayComponent}
|
||||
|
||||
<span className="divider" />
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
ACCESS_STATE_UNKNOWN,
|
||||
VIDEO_QUALITY_AUTO,
|
||||
} from '../constants';
|
||||
import { useNetworkState } from '../hooks/useNetworkState';
|
||||
import { useCallMachine } from './useCallMachine';
|
||||
|
||||
export const CallContext = createContext();
|
||||
|
|
@ -44,6 +45,7 @@ export const CallProvider = ({
|
|||
token,
|
||||
subscribeToTracksAutomatically,
|
||||
});
|
||||
const networkState = useNetworkState(daily, videoQuality);
|
||||
|
||||
// Feature detection taken from daily room object and client browser support
|
||||
useEffect(() => {
|
||||
|
|
@ -107,6 +109,7 @@ export const CallProvider = ({
|
|||
addFakeParticipant,
|
||||
preJoinNonAuthorized,
|
||||
leave,
|
||||
networkState,
|
||||
showLocalVideo,
|
||||
roomExp,
|
||||
videoQuality,
|
||||
|
|
@ -115,8 +118,10 @@ export const CallProvider = ({
|
|||
setBandwidth,
|
||||
setRedirectOnLeave,
|
||||
setShowLocalVideo,
|
||||
setVideoQuality,
|
||||
startCloudRecording,
|
||||
subscribeToTracksAutomatically,
|
||||
videoQuality,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@ import {
|
|||
} from '@dailyjs/shared/contexts/UIStateProvider';
|
||||
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 { useCallState } from './CallProvider';
|
||||
|
|
@ -33,7 +39,7 @@ import {
|
|||
export const ParticipantsContext = createContext();
|
||||
|
||||
export const ParticipantsProvider = ({ children }) => {
|
||||
const { callObject } = useCallState();
|
||||
const { callObject, videoQuality, networkState } = useCallState();
|
||||
const [state, dispatch] = useReducer(
|
||||
participantsReducer,
|
||||
initialParticipantsState
|
||||
|
|
@ -235,34 +241,50 @@ export const ParticipantsProvider = ({ children }) => {
|
|||
}, [callObject, handleNewParticipantsState]);
|
||||
|
||||
/**
|
||||
* Adjust video quality from the 3 simulcast layers based
|
||||
* on active speaker status. Note: this currently uses
|
||||
* undocumented internal methods (we'll be adding support
|
||||
* for this into our API soon!)
|
||||
* Change between the simulcast layers based on view / available bandwidth
|
||||
*/
|
||||
const setBandWidthControls = useCallback(() => {
|
||||
if (typeof rtcpeers === 'undefined') return;
|
||||
const sfu = rtcpeers?.soup;
|
||||
const isSFU = rtcpeers?.currentlyPreferred?.typeName?.() === 'sfu';
|
||||
if (!isSFU) return;
|
||||
if (!(callObject && callObject.meetingState() === 'joined-meeting')) return;
|
||||
|
||||
const ids = participantIds.split(',');
|
||||
const receiveSettings = {};
|
||||
|
||||
ids.forEach((id) => {
|
||||
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
|
||||
if (viewMode === VIEW_MODE_SPEAKER) {
|
||||
if (currentSpeaker?.id === id) {
|
||||
sfu.setPreferredLayerForTrack(id, 'cam-video', 2);
|
||||
receiveSettings[id] = { video: { layer: 2 } };
|
||||
} 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(() => {
|
||||
setBandWidthControls();
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ const MAX_RECENT_SPEAKER_COUNT = 6;
|
|||
* If the remote participant count passes this threshold,
|
||||
* 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);
|
||||
|
||||
|
|
@ -52,135 +52,81 @@ export const TracksProvider = ({ children }) => {
|
|||
[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(
|
||||
() => participants.filter((p) => !p.isLocal).map((p) => p.id),
|
||||
[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.
|
||||
*
|
||||
* @param ids Array of ids to subscribe to, all others will be unsubscribed.
|
||||
* @param pausedIds Array of ids that should be subscribed, but paused.
|
||||
* Updates cam subscriptions based on passed subscribedIds and stagedIds.
|
||||
* For ids not provided, cam tracks will be unsubscribed from
|
||||
*/
|
||||
const updateCamSubscriptions = useCallback(
|
||||
(ids, pausedIds = []) => {
|
||||
if (!callObject || subscribeToTracksAutomatically) return;
|
||||
const subscribedIds =
|
||||
remoteParticipantIds.length <= SUBSCRIBE_ALL_VIDEO_THRESHOLD
|
||||
? [...remoteParticipantIds]
|
||||
: [...ids, ...recentSpeakerIds];
|
||||
(subscribedIds, stagedIds = []) => {
|
||||
if (!callObject) return;
|
||||
|
||||
// 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 shouldSubscribe = subscribedIds.includes(id);
|
||||
const shouldPause = pausedIds.includes(id);
|
||||
const isSubscribed =
|
||||
let desiredSubscription;
|
||||
const currentSubscription =
|
||||
callObject.participants()?.[id]?.tracks?.video?.subscribed;
|
||||
|
||||
/**
|
||||
* Pause already subscribed tracks.
|
||||
*/
|
||||
if (shouldSubscribe && shouldPause) {
|
||||
pauseVideoTrack(id);
|
||||
// Ignore undefined, local or screenshare participant ids
|
||||
if (!id || isLocalId(id) || isScreenId(id)) return u;
|
||||
|
||||
// Decide on desired cam subscription for this participant:
|
||||
// subscribed, staged, or unsubscribed
|
||||
if (subscribedIds.includes(id)) {
|
||||
desiredSubscription = true;
|
||||
} else if (stagedIdsFiltered.includes(id)) {
|
||||
desiredSubscription = 'staged';
|
||||
} else {
|
||||
desiredSubscription = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fast resume tracks.
|
||||
*/
|
||||
if (shouldSubscribe && !shouldPause) {
|
||||
resumeVideoTrack(id);
|
||||
}
|
||||
// Skip if we already have the desired subscription to this
|
||||
// participant's cam
|
||||
if (desiredSubscription === currentSubscription) return u;
|
||||
|
||||
if (
|
||||
isLocalId(id) ||
|
||||
isScreenId(id) ||
|
||||
(shouldSubscribe && isSubscribed)
|
||||
) {
|
||||
return u;
|
||||
}
|
||||
|
||||
const result = {
|
||||
setSubscribedTracks: {
|
||||
audio: true,
|
||||
screenAudio: true,
|
||||
screenVideo: true,
|
||||
video: shouldSubscribe,
|
||||
return {
|
||||
...u,
|
||||
[id]: {
|
||||
setSubscribedTracks: {
|
||||
audio: true,
|
||||
screenAudio: true,
|
||||
screenVideo: true,
|
||||
video: desiredSubscription,
|
||||
},
|
||||
},
|
||||
};
|
||||
return { ...u, [id]: result };
|
||||
}, {});
|
||||
|
||||
callObject.updateParticipants(updates);
|
||||
},
|
||||
[
|
||||
callObject,
|
||||
subscribeToTracksAutomatically,
|
||||
remoteParticipantIds,
|
||||
recentSpeakerIds,
|
||||
pauseVideoTrack,
|
||||
resumeVideoTrack,
|
||||
]
|
||||
[callObject, remoteParticipantIds, recentSpeakerIds]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -273,15 +219,14 @@ export const TracksProvider = ({ children }) => {
|
|||
callObject.off('participant-joined', handleParticipantJoined);
|
||||
callObject.off('participant-left', handleParticipantLeft);
|
||||
};
|
||||
}, [callObject, subscribeToTracksAutomatically, pauseVideoTrack]);
|
||||
}, [callObject, subscribeToTracksAutomatically]);
|
||||
|
||||
return (
|
||||
<TracksContext.Provider
|
||||
value={{
|
||||
audioTracks: state.audioTracks,
|
||||
videoTracks: state.videoTracks,
|
||||
pauseVideoTrack,
|
||||
resumeVideoTrack,
|
||||
subscribeToCam,
|
||||
updateCamSubscriptions,
|
||||
remoteParticipantIds,
|
||||
recentSpeakerIds,
|
||||
|
|
|
|||
|
|
@ -2,23 +2,25 @@ import { useDeepCompareEffect } from 'use-deep-compare';
|
|||
import { useTracks } from '../contexts/TracksProvider';
|
||||
|
||||
/**
|
||||
* Updates cam subscriptions based on passed ids and pausedIds.
|
||||
* @param ids Participant ids which should be subscribed to.
|
||||
* @param pausedIds Participant ids which should be subscribed, but paused.
|
||||
* Updates cam subscriptions based on passed subscribedIds and stagedIds.
|
||||
* @param subscribedIds Participant ids whose cam tracks should be subscribed to.
|
||||
* @param stagedIds Participant ids whose cam tracks should be staged.
|
||||
* @param delay Throttle in milliseconds. Default: 50
|
||||
*/
|
||||
export const useCamSubscriptions = (ids, pausedIds = [], throttle = 50) => {
|
||||
export const useCamSubscriptions = (
|
||||
subscribedIds,
|
||||
stagedIds,
|
||||
throttle = 50
|
||||
) => {
|
||||
const { updateCamSubscriptions } = useTracks();
|
||||
|
||||
useDeepCompareEffect(() => {
|
||||
if (!ids || !pausedIds) return false;
|
||||
if (!subscribedIds || !stagedIds) return false;
|
||||
const timeout = setTimeout(() => {
|
||||
updateCamSubscriptions(ids, pausedIds);
|
||||
updateCamSubscriptions(subscribedIds, stagedIds);
|
||||
}, throttle);
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}, [ids, pausedIds, throttle, updateCamSubscriptions]);
|
||||
return () => clearTimeout(timeout);
|
||||
}, [subscribedIds, stagedIds, throttle, updateCamSubscriptions]);
|
||||
};
|
||||
|
||||
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,
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@daily-co/daily-js": "^0.15.0",
|
||||
"@daily-co/daily-js": "^0.16.0",
|
||||
"bowser": "^2.11.0",
|
||||
"classnames": "^2.3.1",
|
||||
"debounce": "^1.2.1",
|
||||
|
|
|
|||
|
|
@ -160,10 +160,10 @@
|
|||
"@babel/helper-validator-identifier" "^7.12.11"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@daily-co/daily-js@^0.15.0":
|
||||
version "0.15.0"
|
||||
resolved "https://registry.yarnpkg.com/@daily-co/daily-js/-/daily-js-0.15.0.tgz#9dfd5c3ed8855df31c370d5b21a3b5098cce3c4f"
|
||||
integrity sha512-rnivho7yx/yEOtqL81L4daPy9C/FDXf06k06df8vmyUXsE8y+cxSTD7ZvYIJDGJHN6IZRhVxxfbCyPI8CHfwCg==
|
||||
"@daily-co/daily-js@^0.16.0":
|
||||
version "0.16.0"
|
||||
resolved "https://registry.yarnpkg.com/@daily-co/daily-js/-/daily-js-0.16.0.tgz#9020104bb88de62dcc1966e713da65844243b9ab"
|
||||
integrity sha512-DBWzbZs2IR7uYqfbABva1Ms3f/oX85dnQnCpVbGbexTN63LPIGknFSQp31ZYED88qcG+YJNydywBTb+ApNiNXA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
bowser "^2.8.1"
|
||||
|
|
|
|||
Loading…
Reference in New Issue