Get hooks into parity with the current version of prebuilt
This commit is contained in:
parent
3d907d0168
commit
9a39dc2410
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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 />}
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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]);
|
||||||
|
};
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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;
|
|
||||||
Loading…
Reference in New Issue