dailyjs 0.17 bump and receiveSettings methods

This commit is contained in:
Jon 2021-08-17 12:09:39 +01:00
parent 34368d3edf
commit 4e54af6768
12 changed files with 292 additions and 217 deletions

View File

@ -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;
}

View File

@ -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;

View File

@ -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 (

View File

@ -52,10 +52,6 @@ export const BasicTray = () => {
<IconPeople />
</TrayButton>
<TrayButton label="Fake" onClick={() => callObject.addFakeParticipant()}>
+
</TrayButton>
{customTrayComponent}
<span className="divider" />

View File

@ -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}

View File

@ -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();

View File

@ -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,

View File

@ -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;

View File

@ -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;
};

View File

@ -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;

View File

@ -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",

View File

@ -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"