Merge pull request #18 from vector-im/feature/mute-indicators
Add mute indicators
This commit is contained in:
commit
117f6d2f9e
5 changed files with 110 additions and 35 deletions
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
16
src/Room.jsx
16
src/Room.jsx
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue