Merge pull request #367 from robintown/vu-animation

Add a VU meter-style animation to radio mode
This commit is contained in:
Robin 2022-06-01 10:42:07 -04:00 committed by GitHub
commit bab5c9aa42
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 59 additions and 20 deletions

View file

@ -26,6 +26,8 @@ limitations under the License.
--inter-unicode-range: U+0000-20e2, U+20e4-23ce, U+23d0-24c1, U+24c3-259f, --inter-unicode-range: U+0000-20e2, U+20e4-23ce, U+23d0-24c1, U+24c3-259f,
U+25c2-2664, U+2666-2763, U+2765-2b05, U+2b07-2b1b, U+2b1d-10FFFF; U+25c2-2664, U+2666-2763, U+2765-2b05, U+2b07-2b1b, U+2b1d-10FFFF;
--primaryColor: #0dbd8b; --primaryColor: #0dbd8b;
--primaryColor-20: #0dbd8b33;
--alert-20: #ff5b5533;
--bgColor1: #15191e; --bgColor1: #15191e;
--bgColor2: #21262c; --bgColor2: #21262c;
--bgColor3: #444; --bgColor3: #444;

View file

@ -14,14 +14,10 @@
.talking { .talking {
background-color: #0dbd8b; background-color: #0dbd8b;
box-shadow: 0px 0px 0px 17px rgba(13, 189, 139, 0.2),
0px 0px 0px 34px rgba(13, 189, 139, 0.2);
cursor: unset; cursor: unset;
} }
.error { .error {
background-color: #ff5b55; background-color: #ff5b55;
border-color: #ff5b55; border-color: #ff5b55;
box-shadow: 0px 0px 0px 17px rgba(255, 91, 85, 0.2),
0px 0px 0px 34px rgba(255, 91, 85, 0.2);
} }

View file

@ -16,6 +16,7 @@ limitations under the License.
import React, { useCallback, useEffect, useState, createRef } from "react"; import React, { useCallback, useEffect, useState, createRef } from "react";
import classNames from "classnames"; import classNames from "classnames";
import { useSpring, animated } from "@react-spring/web";
import styles from "./PTTButton.module.css"; import styles from "./PTTButton.module.css";
import { ReactComponent as MicIcon } from "../icons/Mic.svg"; import { ReactComponent as MicIcon } from "../icons/Mic.svg";
@ -27,6 +28,7 @@ interface Props {
activeSpeakerDisplayName: string; activeSpeakerDisplayName: string;
activeSpeakerAvatarUrl: string; activeSpeakerAvatarUrl: string;
activeSpeakerIsLocalUser: boolean; activeSpeakerIsLocalUser: boolean;
activeSpeakerVolume: number;
size: number; size: number;
startTalking: () => void; startTalking: () => void;
stopTalking: () => void; stopTalking: () => void;
@ -44,6 +46,7 @@ export const PTTButton: React.FC<Props> = ({
activeSpeakerDisplayName, activeSpeakerDisplayName,
activeSpeakerAvatarUrl, activeSpeakerAvatarUrl,
activeSpeakerIsLocalUser, activeSpeakerIsLocalUser,
activeSpeakerVolume,
size, size,
startTalking, startTalking,
stopTalking, stopTalking,
@ -130,12 +133,32 @@ export const PTTButton: React.FC<Props> = ({
); );
}; };
}, [onWindowMouseUp, onWindowTouchEnd, onButtonTouchStart, buttonRef]); }, [onWindowMouseUp, onWindowTouchEnd, onButtonTouchStart, buttonRef]);
const { shadow } = useSpring({
shadow: (Math.max(activeSpeakerVolume, -70) + 70) * 0.6,
config: {
clamp: true,
tension: 300,
},
});
const shadowColor = showTalkOverError
? "var(--alert-20)"
: "var(--primaryColor-20)";
return ( return (
<button <animated.button
className={classNames(styles.pttButton, { className={classNames(styles.pttButton, {
[styles.talking]: activeSpeakerUserId, [styles.talking]: activeSpeakerUserId,
[styles.error]: showTalkOverError, [styles.error]: showTalkOverError,
})} })}
style={{
boxShadow: shadow.to(
(s) =>
`0px 0px 0px ${s}px ${shadowColor}, 0px 0px 0px ${
2 * s
}px ${shadowColor}`
),
}}
onMouseDown={onButtonMouseDown} onMouseDown={onButtonMouseDown}
ref={buttonRef} ref={buttonRef}
> >
@ -154,6 +177,6 @@ export const PTTButton: React.FC<Props> = ({
className={styles.avatar} className={styles.avatar}
/> />
)} )}
</button> </animated.button>
); );
}; };

View file

@ -123,6 +123,7 @@ export const PTTCallView: React.FC<Props> = ({
talkOverEnabled, talkOverEnabled,
setTalkOverEnabled, setTalkOverEnabled,
activeSpeakerUserId, activeSpeakerUserId,
activeSpeakerVolume,
startTalking, startTalking,
stopTalking, stopTalking,
transmitBlocked, transmitBlocked,
@ -221,6 +222,7 @@ export const PTTCallView: React.FC<Props> = ({
activeSpeakerDisplayName={activeSpeakerDisplayName} activeSpeakerDisplayName={activeSpeakerDisplayName}
activeSpeakerAvatarUrl={activeSpeakerAvatarUrl} activeSpeakerAvatarUrl={activeSpeakerAvatarUrl}
activeSpeakerIsLocalUser={activeSpeakerIsLocalUser} activeSpeakerIsLocalUser={activeSpeakerIsLocalUser}
activeSpeakerVolume={activeSpeakerVolume}
size={pttButtonSize} size={pttButtonSize}
startTalking={startTalking} startTalking={startTalking}
stopTalking={stopTalking} stopTalking={stopTalking}

View file

@ -65,6 +65,7 @@ export interface PTTState {
talkOverEnabled: boolean; talkOverEnabled: boolean;
setTalkOverEnabled: (boolean) => void; setTalkOverEnabled: (boolean) => void;
activeSpeakerUserId: string; activeSpeakerUserId: string;
activeSpeakerVolume: number;
startTalking: () => void; startTalking: () => void;
stopTalking: () => void; stopTalking: () => void;
transmitBlocked: boolean; transmitBlocked: boolean;
@ -108,6 +109,7 @@ export const usePTT = (
isAdmin, isAdmin,
talkOverEnabled, talkOverEnabled,
activeSpeakerUserId, activeSpeakerUserId,
activeSpeakerVolume,
transmitBlocked, transmitBlocked,
}, },
setState, setState,
@ -121,6 +123,7 @@ export const usePTT = (
talkOverEnabled: false, talkOverEnabled: false,
pttButtonHeld: false, pttButtonHeld: false,
activeSpeakerUserId: activeSpeakerFeed ? activeSpeakerFeed.userId : null, activeSpeakerUserId: activeSpeakerFeed ? activeSpeakerFeed.userId : null,
activeSpeakerVolume: -Infinity,
transmitBlocked: false, transmitBlocked: false,
}; };
}); });
@ -152,15 +155,11 @@ export const usePTT = (
playClip(PTTClipID.BLOCKED); playClip(PTTClipID.BLOCKED);
} }
setState((prevState) => { setState((prevState) => ({
return {
...prevState, ...prevState,
activeSpeakerUserId: activeSpeakerFeed activeSpeakerUserId: activeSpeakerFeed ? activeSpeakerFeed.userId : null,
? activeSpeakerFeed.userId
: null,
transmitBlocked: blocked, transmitBlocked: blocked,
}; }));
});
}, [ }, [
playClip, playClip,
groupCall, groupCall,
@ -173,7 +172,7 @@ export const usePTT = (
useEffect(() => { useEffect(() => {
for (const callFeed of userMediaFeeds) { for (const callFeed of userMediaFeeds) {
callFeed.addListener(CallFeedEvent.MuteStateChanged, onMuteStateChanged); callFeed.on(CallFeedEvent.MuteStateChanged, onMuteStateChanged);
} }
const activeSpeakerFeed = getActiveSpeakerFeed(userMediaFeeds, groupCall); const activeSpeakerFeed = getActiveSpeakerFeed(userMediaFeeds, groupCall);
@ -185,14 +184,30 @@ export const usePTT = (
return () => { return () => {
for (const callFeed of userMediaFeeds) { for (const callFeed of userMediaFeeds) {
callFeed.removeListener( callFeed.off(CallFeedEvent.MuteStateChanged, onMuteStateChanged);
CallFeedEvent.MuteStateChanged,
onMuteStateChanged
);
} }
}; };
}, [userMediaFeeds, onMuteStateChanged, groupCall]); }, [userMediaFeeds, onMuteStateChanged, groupCall]);
const onVolumeChanged = useCallback((volume: number) => {
setState((prevState) => ({
...prevState,
activeSpeakerVolume: volume,
}));
}, []);
useEffect(() => {
const activeSpeakerFeed = getActiveSpeakerFeed(userMediaFeeds, groupCall);
activeSpeakerFeed?.on(CallFeedEvent.VolumeChanged, onVolumeChanged);
return () => {
activeSpeakerFeed?.off(CallFeedEvent.VolumeChanged, onVolumeChanged);
setState((prevState) => ({
...prevState,
activeSpeakerVolume: -Infinity,
}));
};
}, [activeSpeakerUserId, onVolumeChanged, userMediaFeeds, groupCall]);
const startTalking = useCallback(async () => { const startTalking = useCallback(async () => {
if (pttButtonHeld) return; if (pttButtonHeld) return;
@ -317,6 +332,7 @@ export const usePTT = (
talkOverEnabled, talkOverEnabled,
setTalkOverEnabled, setTalkOverEnabled,
activeSpeakerUserId, activeSpeakerUserId,
activeSpeakerVolume,
startTalking, startTalking,
stopTalking, stopTalking,
transmitBlocked, transmitBlocked,