Get hooks into parity with the current version of prebuilt

This commit is contained in:
harshithpabbati 2021-11-29 12:54:34 +05:30
parent 3d907d0168
commit 9a39dc2410
14 changed files with 147 additions and 94 deletions

View File

@ -2,7 +2,7 @@ import React, { useMemo } from 'react';
import { Audio } from '@custom/shared/components/Audio'; import { Audio } from '@custom/shared/components/Audio';
import { BasicTray } from '@custom/shared/components/Tray'; import { BasicTray } from '@custom/shared/components/Tray';
import { useParticipants } from '@custom/shared/contexts/ParticipantsProvider'; import { useParticipants } from '@custom/shared/contexts/ParticipantsProvider';
import useJoinSound from '@custom/shared/hooks/useJoinSound'; import { useJoinSound } from '@custom/shared/hooks/useJoinSound';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { WaitingRoom } from './WaitingRoom'; import { WaitingRoom } from './WaitingRoom';

View File

@ -1,5 +1,5 @@
import React, { memo, useEffect, useState, useRef } from 'react'; import React, { memo, useEffect, useState, useRef } from 'react';
import useVideoTrack from '@custom/shared/hooks/useVideoTrack'; import { useVideoTrack } from '@custom/shared/hooks/useVideoTrack';
import { ReactComponent as IconMicMute } from '@custom/shared/icons/mic-off-sm.svg'; import { ReactComponent as IconMicMute } from '@custom/shared/icons/mic-off-sm.svg';
import classNames from 'classnames'; import classNames from 'classnames';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@ -21,7 +21,7 @@ export const Tile = memo(
onVideoResize, onVideoResize,
...props ...props
}) => { }) => {
const videoTrack = useVideoTrack(participant); const videoTrack = useVideoTrack(participant.id);
const videoRef = useRef(null); const videoRef = useRef(null);
const tileRef = useRef(null); const tileRef = useRef(null);
const [tileWidth, setTileWidth] = useState(0); const [tileWidth, setTileWidth] = useState(0);

View File

@ -1,14 +1,11 @@
import React from 'react'; import React from 'react';
import { TrayButton } from '@custom/shared/components/Tray'; import { TrayButton } from '@custom/shared/components/Tray';
import { useAudioLevel } from '@custom/shared/hooks/useAudioLevel';
import { ReactComponent as IconMicOff } from '@custom/shared/icons/mic-off-md.svg'; import { ReactComponent as IconMicOff } from '@custom/shared/icons/mic-off-md.svg';
import { ReactComponent as IconMicOn } from '@custom/shared/icons/mic-on-md.svg'; import { ReactComponent as IconMicOn } from '@custom/shared/icons/mic-on-md.svg';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
export const TrayMicButton = ({ isMuted, onClick }) => { export const TrayMicButton = ({ isMuted, onClick }) => {
const audioLevel = useAudioLevel('local');
return ( return (
<TrayButton label="Mic" onClick={onClick} orange={isMuted}> <TrayButton label="Mic" onClick={onClick} orange={isMuted}>
{isMuted ? <IconMicOff /> : <IconMicOn />} {isMuted ? <IconMicOff /> : <IconMicOn />}

View File

@ -37,6 +37,7 @@ export const CallProvider = ({
const router = useRouter(); const router = useRouter();
const [roomInfo, setRoomInfo] = useState(null); const [roomInfo, setRoomInfo] = useState(null);
const [enableScreenShare, setEnableScreenShare] = useState(false); const [enableScreenShare, setEnableScreenShare] = useState(false);
const [enableJoinSound, setEnableJoinSound] = useState(true);
const [videoQuality, setVideoQuality] = useState(VIDEO_QUALITY_AUTO); const [videoQuality, setVideoQuality] = useState(VIDEO_QUALITY_AUTO);
const [showLocalVideo, setShowLocalVideo] = useState(true); const [showLocalVideo, setShowLocalVideo] = useState(true);
const [preJoinNonAuthorized, setPreJoinNonAuthorized] = useState(false); const [preJoinNonAuthorized, setPreJoinNonAuthorized] = useState(false);
@ -144,6 +145,7 @@ export const CallProvider = ({
roomExp, roomExp,
enableRecording, enableRecording,
enableScreenShare, enableScreenShare,
enableJoinSound,
videoQuality, videoQuality,
setVideoQuality, setVideoQuality,
roomInfo, roomInfo,
@ -154,6 +156,7 @@ export const CallProvider = ({
setEnableScreenShare, setEnableScreenShare,
startCloudRecording, startCloudRecording,
subscribeToTracksAutomatically, subscribeToTracksAutomatically,
setEnableJoinSound
}} }}
> >
{children} {children}

View File

@ -78,10 +78,16 @@ export const useCallMachine = ({
const join = useCallback( const join = useCallback(
async (callObject) => { async (callObject) => {
setState(CALL_STATE_JOINING); setState(CALL_STATE_JOINING);
// Force mute clients when joining a call with experimental_optimize_large_calls enabled.
if (room?.config?.experimental_optimize_large_calls) {
callObject.setLocalAudio(false);
}
await callObject.join({ subscribeToTracksAutomatically, token, url }); await callObject.join({ subscribeToTracksAutomatically, token, url });
setState(CALL_STATE_JOINED); setState(CALL_STATE_JOINED);
}, },
[token, subscribeToTracksAutomatically, url] [room, token, subscribeToTracksAutomatically, url]
); );
/** /**

View File

@ -6,12 +6,12 @@ import { useParticipants } from '../contexts/ParticipantsProvider';
* (= the current one and only actively speaking person) * (= the current one and only actively speaking person)
*/ */
export const useActiveSpeaker = () => { export const useActiveSpeaker = () => {
const { showLocalVideo } = useCallState(); const { broadcastRole, showLocalVideo } = useCallState();
const { activeParticipant, localParticipant, participantCount } = const { activeParticipant, localParticipant, participantCount } =
useParticipants(); useParticipants();
// we don't show active speaker indicators EVER in a 1:1 call or when the user is alone in-call // 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 (broadcastRole !== 'attendee' && participantCount <= 2) return null;
if (!activeParticipant?.isMicMuted) { if (!activeParticipant?.isMicMuted) {
return activeParticipant?.id; return activeParticipant?.id;

View File

@ -1,36 +1,54 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import getConfig from 'next/config';
export const useAudioLevel = (sessionId) => { export const useAudioLevel = (stream) => {
const [audioLevel, setAudioLevel] = useState(0); const [micVolume, setMicVolume] = useState(0);
const { assetPrefix } = getConfig().publicRuntimeConfig;
useEffect(() => { useEffect(() => {
if (!sessionId) { if (!stream) {
return false; setMicVolume(0);
return;
} }
const AudioCtx =
typeof AudioContext !== 'undefined'
? AudioContext
: typeof webkitAudioContext !== 'undefined'
? webkitAudioContext
: null;
if (!AudioCtx) return;
const audioContext = new AudioCtx();
const mediaStreamSource = audioContext.createMediaStreamSource(stream);
let node;
const i = setInterval(async () => { const startProcessing = async () => {
try { try {
if (!(window.rtcpeers && window.rtcpeers.sfu)) { await audioContext.audioWorklet.addModule(
return; `${assetPrefix}/audiolevel-processor.js`
} );
const consumer =
window.rtcpeers.sfu.consumers[`${sessionId}/cam-audio`];
if (!(consumer && consumer.getStats)) {
return;
}
const level = Array.from((await consumer.getStats()).values()).find(
(s) => 'audioLevel' in s
).audioLevel;
setAudioLevel(level);
} catch (e) {
console.error(e);
}
}, 2000);
return () => clearInterval(i); node = new AudioWorkletNode(audioContext, 'audiolevel');
}, [sessionId]);
return audioLevel; node.port.onmessage = (event) => {
}; let volume = 0;
if (event.data.volume) volume = event.data.volume;
if (!node) return;
setMicVolume(volume);
};
export default useAudioLevel; mediaStreamSource.connect(node).connect(audioContext.destination);
} catch {}
};
startProcessing();
return () => {
node?.disconnect();
node = null;
mediaStreamSource?.disconnect();
audioContext?.close();
};
}, [assetPrefix, stream]);
return micVolume;
};

View File

@ -2,14 +2,13 @@ import { useDeepCompareMemo } from 'use-deep-compare';
import { useTracks } from '../contexts/TracksProvider'; import { useTracks } from '../contexts/TracksProvider';
export const useAudioTrack = (participant) => { export const useAudioTrack = (id) => {
const { audioTracks } = useTracks(); const { audioTracks } = useTracks();
return useDeepCompareMemo(() => { return useDeepCompareMemo(() => {
const audioTrack = audioTracks?.[participant?.id]; const audioTrack = audioTracks?.[id];
// @ts-ignore
return audioTrack?.persistentTrack; return audioTrack?.persistentTrack;
}, [participant?.id, audioTracks]); }, [id, audioTracks]);
}; };
export default useAudioTrack; export default useAudioTrack;

View File

@ -15,12 +15,10 @@ export const useCamSubscriptions = (
const { updateCamSubscriptions } = useTracks(); const { updateCamSubscriptions } = useTracks();
useDeepCompareEffect(() => { useDeepCompareEffect(() => {
if (!subscribedIds || !stagedIds) return false; if (!subscribedIds || !stagedIds) return;
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
updateCamSubscriptions(subscribedIds, stagedIds); updateCamSubscriptions(subscribedIds, stagedIds);
}, throttle); }, throttle);
return () => clearTimeout(timeout); return () => clearTimeout(timeout);
}, [subscribedIds, stagedIds, throttle, updateCamSubscriptions]); }, [subscribedIds, stagedIds, throttle, updateCamSubscriptions]);
}; };
export default useCamSubscriptions;

View File

@ -1,39 +1,42 @@
import { useEffect, useMemo } from 'react'; import { useEffect, useState } from 'react';
import { debounce } from 'debounce';
import { useCallState } from '../contexts/CallProvider'; import { useCallState } from '../contexts/CallProvider';
import { useSound } from './useSound'; import { useSoundLoader } from './useSoundLoader';
/** /**
* Convenience hook to play `join.mp3` when participants join the call * Convenience hook to play `join.mp3` when first other participants joins.
*/ */
export const useJoinSound = () => { export const useJoinSound = () => {
const { callObject } = useCallState(); const { callObject: daily } = useCallState();
const { load, play } = useSound('assets/join.mp3'); const { joinSound } = useSoundLoader();
const [playJoinSound, setPlayJoinSound] = useState(false);
useEffect(() => { useEffect(() => {
load(); if (!daily) return;
}, [load]); /**
* We don't want to immediately play a joined sound, when the user joins the meeting:
const debouncedPlay = useMemo(() => debounce(() => play(), 200), [play]); * Upon joining all other participants, that were already in-call, will emit a
* participant-joined event.
useEffect(() => { * In waiting 2 seconds we make sure, that the sound is only played when the user
if (!callObject) return false; * is **really** the first participant.
*/
const handleParticipantJoined = () => {
debouncedPlay();
};
callObject.on('participant-joined', handleParticipantJoined);
setTimeout(() => { setTimeout(() => {
handleParticipantJoined(); setPlayJoinSound(true);
}, 2000); }, 2000);
}, [daily]);
return () => { useEffect(() => {
callObject.off('participant-joined', handleParticipantJoined); if (!daily) return;
const handleParticipantJoined = () => {
// first other participant joined --> play sound
if (!playJoinSound || Object.keys(daily.participants()).length !== 2)
return;
joinSound.play();
}; };
}, [callObject, debouncedPlay]);
};
export default useJoinSound; daily.on('participant-joined', handleParticipantJoined);
return () => {
daily.off('participant-joined', handleParticipantJoined);
};
}, [daily, joinSound, playJoinSound]);
};

View File

@ -1,4 +1,3 @@
/* global rtcpeers */
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { import {
@ -21,10 +20,10 @@ export const useNetworkState = (
const setQuality = useCallback( const setQuality = useCallback(
(q) => { (q) => {
if (!callObject || typeof rtcpeers === 'undefined') return; if (!callObject) return;
const peers = Object.keys(callObject.participants()).length - 1; const peers = Object.keys(callObject.participants()).length - 1;
const isSFU = rtcpeers?.currentlyPreferred?.typeName?.() === 'sfu'; const isSFU = callObject.getNetworkTopology().topology === 'sfu';
const lowKbs = isSFU const lowKbs = isSFU
? STANDARD_LOW_BITRATE_CAP ? STANDARD_LOW_BITRATE_CAP

View File

@ -1,6 +1,8 @@
import { useCallback, useEffect, useRef } from 'react'; import { useCallback, useEffect, useRef } from 'react';
export const useSound = (src) => { const defaultNotMuted = () => false;
export const useSound = (src, isMuted = defaultNotMuted) => {
const audio = useRef(null); const audio = useRef(null);
useEffect(() => { useEffect(() => {
@ -22,17 +24,15 @@ export const useSound = (src) => {
audio.current.load(); audio.current.load();
}, [audio]); }, [audio]);
const play = useCallback(() => { const play = useCallback(async () => {
if (!audio.current) return; if (!audio.current || isMuted()) return;
try { try {
audio.current.currentTime = 0; audio.current.currentTime = 0;
audio.current.play(); await audio.current.play();
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
}, [audio]); }, [audio, isMuted]);
return { load, play }; return { load, play };
}; };
export default useSound;

View File

@ -0,0 +1,24 @@
import { useCallback, useMemo } from 'react';
import { useCallState } from '../contexts/CallProvider';
import { useSound } from './useSound';
export const useSoundLoader = () => {
const { enableJoinSound } = useCallState();
const isJoinSoundMuted = useCallback(
() => !enableJoinSound,
[enableJoinSound]
);
const joinSound = useSound(`assets/join.mp3`, isJoinSoundMuted);
const load = useCallback(() => {
joinSound.load();
}, [joinSound]);
return useMemo(
() => ({ joinSound, load }),
[joinSound, load]
);
};

View File

@ -1,22 +1,28 @@
import { useDeepCompareMemo } from 'use-deep-compare'; import { useDeepCompareMemo } from 'use-deep-compare';
import { useTracks } from '../contexts/TracksProvider';
import { DEVICE_STATE_BLOCKED, DEVICE_STATE_OFF } from '../contexts/useDevices';
export const useVideoTrack = (participant) => { import { useTracks } from '../contexts/TracksProvider';
import { isLocalId, isScreenId } from '../contexts/participantsState';
export const useVideoTrack = (id) => {
const { videoTracks } = useTracks(); const { videoTracks } = useTracks();
const videoTrack = useDeepCompareMemo(
() => videoTracks?.[id],
[id, videoTracks]
);
/**
* MediaStreamTrack's are difficult to compare.
* Changes to a video track's id will likely need to be reflected in the UI / DOM.
* This usually happens on P2P / SFU switches.
*/
return useDeepCompareMemo(() => { return useDeepCompareMemo(() => {
const videoTrack = videoTracks?.[participant?.id];
if ( if (
videoTrack?.state === DEVICE_STATE_OFF || videoTrack?.state === 'off' ||
videoTrack?.state === DEVICE_STATE_BLOCKED || videoTrack?.state === 'blocked' ||
(!videoTrack?.subscribed && (!videoTrack?.subscribed && !isLocalId(id) && !isScreenId(id))
participant?.id !== 'local' &&
!participant.isScreenshare)
) )
return null; return null;
return videoTrack?.persistentTrack; return videoTrack?.persistentTrack;
}, [participant?.id, videoTracks]); }, [id, videoTrack]);
}; };
export default useVideoTrack;