Merge pull request #18 from vector-im/feature/mute-indicators

Add mute indicators
This commit is contained in:
Robert Long 2021-08-23 15:45:04 -07:00 committed by GitHub
commit 117f6d2f9e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 110 additions and 35 deletions

View file

@ -172,7 +172,7 @@ export class ConferenceCallManager extends EventEmitter {
this.localVideoStream = null; this.localVideoStream = null;
this.localParticipant = null; this.localParticipant = null;
this.micMuted = false; this.audioMuted = false;
this.videoMuted = false; this.videoMuted = false;
this.client.on("RoomState.members", this._onRoomStateMembers); this.client.on("RoomState.members", this._onRoomStateMembers);
@ -245,6 +245,8 @@ export class ConferenceCallManager extends EventEmitter {
sessionId: this.sessionId, sessionId: this.sessionId,
call: null, call: null,
stream, stream,
audioMuted: this.audioMuted,
videoMuted: this.videoMuted,
}; };
this.participants.push(this.localParticipant); this.participants.push(this.localParticipant);
@ -312,7 +314,9 @@ export class ConferenceCallManager extends EventEmitter {
this.participants = []; this.participants = [];
this.localParticipant.stream = null; this.localParticipant.stream = null;
this.localParticipant.call = null; this.localParticipant.call = null;
this.micMuted = false; this.localParticipant.audioMuted = false;
this.localParticipant.videoMuted = false;
this.audioMuted = false;
this.videoMuted = false; this.videoMuted = false;
clearTimeout(this._memberParticipantStateTimeout); clearTimeout(this._memberParticipantStateTimeout);
@ -332,15 +336,19 @@ export class ConferenceCallManager extends EventEmitter {
return stream; return stream;
} }
setMicMuted(muted) { setAudioMuted(muted) {
this.micMuted = muted; this.audioMuted = muted;
if (this.localParticipant) {
this.localParticipant.audioMuted = muted;
}
const localStream = this.localVideoStream; const localStream = this.localVideoStream;
if (localStream) { if (localStream) {
for (const track of localStream.getTracks()) { for (const track of localStream.getTracks()) {
if (track.kind === "audio") { if (track.kind === "audio") {
track.enabled = !this.micMuted; track.enabled = !this.audioMuted;
} }
} }
} }
@ -351,16 +359,22 @@ export class ConferenceCallManager extends EventEmitter {
if ( if (
call && call &&
call.localUsermediaStream && call.localUsermediaStream &&
call.isMicrophoneMuted() !== this.micMuted call.isMicrophoneMuted() !== this.audioMuted
) { ) {
call.setMicrophoneMuted(this.micMuted); call.setMicrophoneMuted(this.audioMuted);
} }
} }
this.emit("participants_changed");
} }
setVideoMuted(muted) { setVideoMuted(muted) {
this.videoMuted = muted; this.videoMuted = muted;
if (this.localParticipant) {
this.localParticipant.videoMuted = muted;
}
const localStream = this.localVideoStream; const localStream = this.localVideoStream;
if (localStream) { if (localStream) {
@ -382,6 +396,8 @@ export class ConferenceCallManager extends EventEmitter {
call.setLocalVideoMuted(this.videoMuted); call.setLocalVideoMuted(this.videoMuted);
} }
} }
this.emit("participants_changed");
} }
logout() { logout() {
@ -473,7 +489,16 @@ export class ConferenceCallManager extends EventEmitter {
} }
// Get the remote video stream if it exists. // Get the remote video stream if it exists.
const stream = call.getRemoteFeeds()[0]?.stream; const remoteFeed = call.getRemoteFeeds()[0];
const stream = remoteFeed && remoteFeed.stream;
const audioMuted = remoteFeed ? remoteFeed.isAudioMuted() : false;
const videoMuted = remoteFeed ? remoteFeed.isVideoMuted() : false;
if (remoteFeed) {
remoteFeed.on("mute_state_changed", () =>
this._onCallFeedMuteStateChanged(participant, remoteFeed)
);
}
const userId = call.opponentMember.userId; const userId = call.opponentMember.userId;
@ -496,6 +521,8 @@ export class ConferenceCallManager extends EventEmitter {
existingParticipant.call.hangup("replaced", false); existingParticipant.call.hangup("replaced", false);
existingParticipant.call = call; existingParticipant.call = call;
existingParticipant.stream = stream; existingParticipant.stream = stream;
existingParticipant.audioMuted = audioMuted;
existingParticipant.videoMuted = videoMuted;
existingParticipant.sessionId = sessionId; existingParticipant.sessionId = sessionId;
} else { } else {
participant = { participant = {
@ -504,6 +531,8 @@ export class ConferenceCallManager extends EventEmitter {
sessionId, sessionId,
call, call,
stream, stream,
audioMuted,
videoMuted,
}; };
this.participants.push(participant); this.participants.push(participant);
} }
@ -592,6 +621,8 @@ export class ConferenceCallManager extends EventEmitter {
participant.sessionId = sessionId; participant.sessionId = sessionId;
participant.call = call; participant.call = call;
participant.stream = null; participant.stream = null;
participant.audioMuted = false;
participant.videoMuted = false;
} else { } else {
participant = { participant = {
local: false, local: false,
@ -599,6 +630,8 @@ export class ConferenceCallManager extends EventEmitter {
sessionId, sessionId,
call, call,
stream: null, stream: null,
audioMuted: false,
videoMuted: false,
}; };
// TODO: Should we wait until the call has been answered to push the participant? // TODO: Should we wait until the call has been answered to push the participant?
// Or do we hide the participant until their stream is live? // Or do we hide the participant until their stream is live?
@ -630,9 +663,9 @@ export class ConferenceCallManager extends EventEmitter {
_onCallStateChanged = (participant, call, state) => { _onCallStateChanged = (participant, call, state) => {
if ( if (
call.localUsermediaStream && call.localUsermediaStream &&
call.isMicrophoneMuted() !== this.micMuted call.isMicrophoneMuted() !== this.audioMuted
) { ) {
call.setMicrophoneMuted(this.micMuted); call.setMicrophoneMuted(this.audioMuted);
} }
if ( if (
@ -646,14 +679,28 @@ export class ConferenceCallManager extends EventEmitter {
}; };
_onCallFeedsChanged = (participant, call) => { _onCallFeedsChanged = (participant, call) => {
const feeds = call.getRemoteFeeds(); const remoteFeed = call.getRemoteFeeds()[0];
const stream = remoteFeed && remoteFeed.stream;
const audioMuted = remoteFeed ? remoteFeed.isAudioMuted() : false;
const videoMuted = remoteFeed ? remoteFeed.isVideoMuted() : false;
if (feeds.length > 0 && participant.stream !== feeds[0].stream) { if (remoteFeed && participant.stream !== stream) {
participant.stream = feeds[0].stream; participant.stream = stream;
this.emit("participants_changed"); participant.audioMuted = audioMuted;
participant.videoMuted = videoMuted;
remoteFeed.on("mute_state_changed", () =>
this._onCallFeedMuteStateChanged(participant, remoteFeed)
);
this._onCallFeedMuteStateChanged(participant, remoteFeed);
} }
}; };
_onCallFeedMuteStateChanged = (participant, feed) => {
participant.audioMuted = feed.isAudioMuted();
participant.videoMuted = feed.isVideoMuted();
this.emit("participants_changed");
};
_onCallReplaced = (participant, call, newCall) => { _onCallReplaced = (participant, call, newCall) => {
participant.call = newCall; participant.call = newCall;
@ -668,10 +715,15 @@ export class ConferenceCallManager extends EventEmitter {
); );
newCall.on("hangup", () => this._onCallHangup(participant, newCall)); newCall.on("hangup", () => this._onCallHangup(participant, newCall));
const feeds = newCall.getRemoteFeeds(); const remoteFeed = newCall.getRemoteFeeds()[0];
participant.stream = remoteFeed ? remoteFeed.stream : null;
participant.audioMuted = remoteFeed ? remoteFeed.isAudioMuted() : false;
participant.videoMuted = remoteFeed ? remoteFeed.isVideoMuted() : false;
if (feeds.length > 0) { if (remoteFeed) {
participant.stream = feeds[0].stream; remoteFeed.on("mute_state_changed", () =>
this._onCallFeedMuteStateChanged(participant, remoteFeed)
);
} }
this.emit("call", newCall); this.emit("call", newCall);

View file

@ -148,7 +148,7 @@ export function useVideoRoom(manager, roomId, timeout = 5000) {
participants, participants,
error, error,
videoMuted, videoMuted,
micMuted, audioMuted,
}, },
setState, setState,
] = useState({ ] = useState({
@ -159,7 +159,7 @@ export function useVideoRoom(manager, roomId, timeout = 5000) {
participants: [], participants: [],
error: undefined, error: undefined,
videoMuted: false, videoMuted: false,
micMuted: false, audioMuted: false,
}); });
useEffect(() => { useEffect(() => {
@ -225,7 +225,7 @@ export function useVideoRoom(manager, roomId, timeout = 5000) {
setState((prevState) => ({ setState((prevState) => ({
...prevState, ...prevState,
videoMuted: manager.videoMuted, videoMuted: manager.videoMuted,
micMuted: manager.micMuted, audioMuted: manager.audioMuted,
})); }));
} }
@ -329,9 +329,9 @@ export function useVideoRoom(manager, roomId, timeout = 5000) {
}; };
}, [manager]); }, [manager]);
const toggleMuteMic = useCallback(() => { const toggleMuteAudio = useCallback(() => {
manager.setMicMuted(!manager.micMuted); manager.setAudioMuted(!manager.audioMuted);
setState((prevState) => ({ ...prevState, micMuted: manager.micMuted })); setState((prevState) => ({ ...prevState, audioMuted: manager.audioMuted }));
}, [manager]); }, [manager]);
const toggleMuteVideo = useCallback(() => { const toggleMuteVideo = useCallback(() => {
@ -349,9 +349,9 @@ export function useVideoRoom(manager, roomId, timeout = 5000) {
joinCall, joinCall,
leaveCall, leaveCall,
toggleMuteVideo, toggleMuteVideo,
toggleMuteMic, toggleMuteAudio,
videoMuted, videoMuted,
micMuted, audioMuted,
}; };
} }

View file

@ -47,9 +47,9 @@ export function Room({ manager }) {
joinCall, joinCall,
leaveCall, leaveCall,
toggleMuteVideo, toggleMuteVideo,
toggleMuteMic, toggleMuteAudio,
videoMuted, videoMuted,
micMuted, audioMuted,
} = useVideoRoom(manager, roomId); } = useVideoRoom(manager, roomId);
const debugStr = query.get("debug"); const debugStr = query.get("debug");
const [debug, setDebug] = useState(debugStr === "" || debugStr === "true"); const [debug, setDebug] = useState(debugStr === "" || debugStr === "true");
@ -100,9 +100,9 @@ export function Room({ manager }) {
joining={joining} joining={joining}
joinCall={joinCall} joinCall={joinCall}
toggleMuteVideo={toggleMuteVideo} toggleMuteVideo={toggleMuteVideo}
toggleMuteMic={toggleMuteMic} toggleMuteAudio={toggleMuteAudio}
videoMuted={videoMuted} videoMuted={videoMuted}
micMuted={micMuted} audioMuted={audioMuted}
/> />
)} )}
{!loading && room && joined && participants.length === 0 && ( {!loading && room && joined && participants.length === 0 && (
@ -115,7 +115,7 @@ export function Room({ manager }) {
)} )}
{!loading && room && joined && ( {!loading && room && joined && (
<div className={styles.footer}> <div className={styles.footer}>
<MicButton muted={micMuted} onClick={toggleMuteMic} /> <MicButton muted={audioMuted} onClick={toggleMuteAudio} />
<VideoButton enabled={videoMuted} onClick={toggleMuteVideo} /> <VideoButton enabled={videoMuted} onClick={toggleMuteVideo} />
<HangupButton onClick={leaveCall} /> <HangupButton onClick={leaveCall} />
</div> </div>
@ -130,9 +130,9 @@ function JoinRoom({
joinCall, joinCall,
manager, manager,
toggleMuteVideo, toggleMuteVideo,
toggleMuteMic, toggleMuteAudio,
videoMuted, videoMuted,
micMuted, audioMuted,
}) { }) {
const videoRef = useRef(); const videoRef = useRef();
const [hasPermissions, setHasPermissions] = useState(false); const [hasPermissions, setHasPermissions] = useState(false);
@ -167,7 +167,7 @@ function JoinRoom({
</div> </div>
{hasPermissions && ( {hasPermissions && (
<div className={styles.previewButtons}> <div className={styles.previewButtons}>
<MicButton muted={micMuted} onClick={toggleMuteMic} /> <MicButton muted={audioMuted} onClick={toggleMuteAudio} />
<VideoButton enabled={videoMuted} onClick={toggleMuteVideo} /> <VideoButton enabled={videoMuted} onClick={toggleMuteVideo} />
</div> </div>
)} )}

View file

@ -6,6 +6,8 @@ import styles from "./VideoGrid.module.css";
import useMeasure from "react-use-measure"; import useMeasure from "react-use-measure";
import moveArrItem from "lodash-move"; import moveArrItem from "lodash-move";
import { ReactComponent as MicIcon } from "./icons/Mic.svg"; import { ReactComponent as MicIcon } from "./icons/Mic.svg";
import { ReactComponent as MuteMicIcon } from "./icons/MuteMic.svg";
import { ReactComponent as DisableVideoIcon } from "./icons/DisableVideo.svg";
function useIsMounted() { function useIsMounted() {
const isMountedRef = useRef(false); const isMountedRef = useRef(false);
@ -356,9 +358,20 @@ function ParticipantTile({ style, participant, remove, ...rest }) {
[styles.speaking]: participant.speaking, [styles.speaking]: participant.speaking,
})} })}
> >
{participant.speaking && <MicIcon />} {participant.speaking ? (
<MicIcon />
) : participant.audioMuted ? (
<MuteMicIcon className={styles.muteMicIcon} />
) : null}
<span>{participant.userId}</span> <span>{participant.userId}</span>
</div> </div>
{participant.videoMuted && (
<DisableVideoIcon
className={styles.videoMuted}
width={48}
height={48}
/>
)}
<video ref={videoRef} playsInline disablePictureInPicture /> <video ref={videoRef} playsInline disablePictureInPicture />
</animated.div> </animated.div>
); );

View file

@ -62,7 +62,17 @@ limitations under the License.
line-height: 32px; line-height: 32px;
} }
.muteMicIcon * {
fill: #FF5B55;
}
.videoMuted {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.participantTile:hover .participantName, .participantName.speaking { .participantTile:hover .participantName, .participantName.speaking {
display: flex; display: flex;
} }