Add video/mic mute

This commit is contained in:
Robert Long 2021-08-20 14:43:16 -07:00
parent cde692f10d
commit 5851d738f8
5 changed files with 269 additions and 174 deletions

View File

@ -9287,7 +9287,7 @@ module.exports={
"𝐴": "A", "𝐴": "A",
"𝑨": "A", "𝑨": "A",
"𝒜": "A", "𝒜": "A",
"𝓐": "A", "<EFBFBD><EFBFBD><EFBFBD>": "A",
"𝔄": "A", "𝔄": "A",
"𝔸": "A", "𝔸": "A",
"𝕬": "A", "𝕬": "A",
@ -13735,7 +13735,7 @@ module.exports={
"⽒": "氏", "⽒": "氏",
"⺠": "民", "⺠": "民",
"⽓": "气", "⽓": "气",
"<EFBFBD><EFBFBD><EFBFBD>": "水", "": "水",
"⺡": "氵", "⺡": "氵",
"⺢": "氺", "⺢": "氺",
"汎": "汎", "汎": "汎",
@ -58557,7 +58557,6 @@ class MatrixCall extends events_1.EventEmitter {
} }
this.pushLocalFeed(stream, callEventTypes_1.SDPStreamMetadataPurpose.Usermedia); this.pushLocalFeed(stream, callEventTypes_1.SDPStreamMetadataPurpose.Usermedia);
this.setState(CallState.CreateOffer); this.setState(CallState.CreateOffer);
logger_1.logger.info("Got local AV stream with id " + this.localUsermediaStream.id);
logger_1.logger.debug("gotUserMediaForInvite -> " + this.type); logger_1.logger.debug("gotUserMediaForInvite -> " + this.type);
// Now we wait for the negotiationneeded event // Now we wait for the negotiationneeded event
}); });
@ -58566,7 +58565,6 @@ class MatrixCall extends events_1.EventEmitter {
return; return;
} }
this.pushLocalFeed(stream, callEventTypes_1.SDPStreamMetadataPurpose.Usermedia); this.pushLocalFeed(stream, callEventTypes_1.SDPStreamMetadataPurpose.Usermedia);
logger_1.logger.info("Got local AV stream with id " + this.localUsermediaStream.id);
this.setState(CallState.CreateAnswer); this.setState(CallState.CreateAnswer);
let myAnswer; let myAnswer;
try { try {
@ -59007,6 +59005,7 @@ class MatrixCall extends events_1.EventEmitter {
this.feeds.push(new callFeed_1.CallFeed(stream, userId, purpose, this.client, this.roomId, false, false)); this.feeds.push(new callFeed_1.CallFeed(stream, userId, purpose, this.client, this.roomId, false, false));
this.emit(CallEvent.FeedsChanged, this.feeds); this.emit(CallEvent.FeedsChanged, this.feeds);
} }
// TODO: Find out what is going on here
// why do we enable audio (and only audio) tracks here? -- matthew // why do we enable audio (and only audio) tracks here? -- matthew
setTracksEnabled(stream.getAudioTracks(), true); setTracksEnabled(stream.getAudioTracks(), true);
if (addToPeerConnection) { if (addToPeerConnection) {
@ -59169,9 +59168,6 @@ class MatrixCall extends events_1.EventEmitter {
return; return;
} }
} }
else if (this.localUsermediaStream) {
this.gotUserMediaForAnswer(this.localUsermediaStream);
}
else if (this.waitForLocalAVStream) { else if (this.waitForLocalAVStream) {
this.setState(CallState.WaitLocalMedia); this.setState(CallState.WaitLocalMedia);
} }
@ -59183,16 +59179,11 @@ class MatrixCall extends events_1.EventEmitter {
* @param {MatrixCall} newCall The new call. * @param {MatrixCall} newCall The new call.
*/ */
replacedBy(newCall) { replacedBy(newCall) {
logger_1.logger.debug(this.callId + " being replaced by " + newCall.callId);
if (this.state === CallState.WaitLocalMedia) { if (this.state === CallState.WaitLocalMedia) {
logger_1.logger.debug("Telling new call to wait for local media"); logger_1.logger.debug("Telling new call to wait for local media");
newCall.waitForLocalAVStream = true; newCall.waitForLocalAVStream = true;
} }
else if (this.state === CallState.CreateOffer) { else if ([CallState.CreateOffer, CallState.InviteSent].includes(this.state)) {
logger_1.logger.debug("Handing local stream to new call");
newCall.gotUserMediaForAnswer(this.localUsermediaStream);
}
else if (this.state === CallState.InviteSent) {
logger_1.logger.debug("Handing local stream to new call"); logger_1.logger.debug("Handing local stream to new call");
newCall.gotUserMediaForAnswer(this.localUsermediaStream); newCall.gotUserMediaForAnswer(this.localUsermediaStream);
} }
@ -59841,10 +59832,8 @@ class MatrixCall extends events_1.EventEmitter {
stopAllMedia() { stopAllMedia() {
logger_1.logger.debug(`stopAllMedia (stream=${this.localUsermediaStream})`); logger_1.logger.debug(`stopAllMedia (stream=${this.localUsermediaStream})`);
for (const feed of this.feeds) { for (const feed of this.feeds) {
if (!feed.isLocal()) { for (const track of feed.stream.getTracks()) {
for (const track of feed.stream.getTracks()) { track.stop();
track.stop();
}
} }
} }
} }
@ -60287,157 +60276,161 @@ class CallEventHandler {
return type.startsWith("m.call.") || type.startsWith("org.matrix.call."); return type.startsWith("m.call.") || type.startsWith("org.matrix.call.");
} }
handleCallEvent(event) { handleCallEvent(event) {
const content = event.getContent(); return __awaiter(this, void 0, void 0, function* () {
const type = event.getType(); const content = event.getContent();
const weSentTheEvent = event.getSender() === this.client.credentials.userId; const type = event.getType();
let call = content.call_id ? this.calls.get(content.call_id) : undefined; const weSentTheEvent = event.getSender() === this.client.credentials.userId;
//console.info("RECV %s content=%s", type, JSON.stringify(content)); let call = content.call_id ? this.calls.get(content.call_id) : undefined;
if (type === event_1.EventType.CallInvite) { //console.info("RECV %s content=%s", type, JSON.stringify(content));
// ignore invites you send if (type === event_1.EventType.CallInvite) {
if (weSentTheEvent) // ignore invites you send
return; if (weSentTheEvent)
// expired call return;
if (event.getLocalAge() > content.lifetime - RING_GRACE_PERIOD) // expired call
return; if (event.getLocalAge() > content.lifetime - RING_GRACE_PERIOD)
// stale/old invite event return;
if (call && call.state === call_1.CallState.Ended) // stale/old invite event
return; if (call && call.state === call_1.CallState.Ended)
if (call) { return;
logger_1.logger.log(`WARN: Already have a MatrixCall with id ${content.call_id} but got an ` +
`invite. Clobbering.`);
}
if (content.invitee && content.invitee !== this.client.getUserId()) {
return; // This invite was meant for another user in the room
}
const timeUntilTurnCresExpire = this.client.getTurnServersExpiry() - Date.now();
logger_1.logger.info("Current turn creds expire in " + timeUntilTurnCresExpire + " ms");
call = call_1.createNewMatrixCall(this.client, event.getRoomId(), { forceTURN: this.client.forceTURN });
if (!call) {
logger_1.logger.log("Incoming call ID " + content.call_id + " but this client " +
"doesn't support WebRTC");
// don't hang up the call: there could be other clients
// connected that do support WebRTC and declining the
// the call on their behalf would be really annoying.
return;
}
call.callId = content.call_id;
const invitePromise = call.initWithInvite(event);
this.calls.set(call.callId, call);
// if we stashed candidate events for that call ID, play them back now
if (this.candidateEventsByCall.get(call.callId)) {
for (const ev of this.candidateEventsByCall.get(call.callId)) {
call.onRemoteIceCandidatesReceived(ev);
}
}
// Were we trying to call that user (room)?
let existingCall;
for (const thisCall of this.calls.values()) {
const isCalling = [call_1.CallState.WaitLocalMedia, call_1.CallState.CreateOffer, call_1.CallState.InviteSent].includes(thisCall.state);
if (call.roomId === thisCall.roomId &&
thisCall.direction === call_1.CallDirection.Outbound &&
call.invitee === thisCall.invitee &&
isCalling) {
existingCall = thisCall;
break;
}
}
if (existingCall) {
// If we've only got to wait_local_media or create_offer and
// we've got an invite, pick the incoming call because we know
// we haven't sent our invite yet otherwise, pick whichever
// call has the lowest call ID (by string comparison)
if (existingCall.state === call_1.CallState.WaitLocalMedia ||
existingCall.state === call_1.CallState.CreateOffer ||
existingCall.callId > call.callId) {
logger_1.logger.log("Glare detected: answering incoming call " + call.callId +
" and canceling outgoing call " + existingCall.callId);
existingCall.replacedBy(call);
call.answer();
}
else {
logger_1.logger.log("Glare detected: rejecting incoming call " + call.callId +
" and keeping outgoing call " + existingCall.callId);
call.hangup(call_1.CallErrorCode.Replaced, true);
}
}
else {
invitePromise.then(() => {
this.client.emit("Call.incoming", call);
});
}
}
else if (type === event_1.EventType.CallCandidates) {
if (weSentTheEvent)
return;
if (!call) {
// store the candidates; we may get a call eventually.
if (!this.candidateEventsByCall.has(content.call_id)) {
this.candidateEventsByCall.set(content.call_id, []);
}
this.candidateEventsByCall.get(content.call_id).push(event);
}
else {
call.onRemoteIceCandidatesReceived(event);
}
}
else if ([event_1.EventType.CallHangup, event_1.EventType.CallReject].includes(type)) {
// Note that we also observe our own hangups here so we can see
// if we've already rejected a call that would otherwise be valid
if (!call) {
// if not live, store the fact that the call has ended because
// we're probably getting events backwards so
// the hangup will come before the invite
call = call_1.createNewMatrixCall(this.client, event.getRoomId());
if (call) { if (call) {
call.callId = content.call_id; logger_1.logger.log(`WARN: Already have a MatrixCall with id ${content.call_id} but got an ` +
call.initWithHangup(event); `invite. Clobbering.`);
this.calls.set(content.call_id, call);
} }
} if (content.invitee && content.invitee !== this.client.getUserId()) {
else { return; // This invite was meant for another user in the room
if (call.state !== call_1.CallState.Ended) { }
if (type === event_1.EventType.CallHangup) { const timeUntilTurnCresExpire = this.client.getTurnServersExpiry() - Date.now();
call.onHangupReceived(content); logger_1.logger.info("Current turn creds expire in " + timeUntilTurnCresExpire + " ms");
call = call_1.createNewMatrixCall(this.client, event.getRoomId(), { forceTURN: this.client.forceTURN });
if (!call) {
logger_1.logger.log("Incoming call ID " + content.call_id + " but this client " +
"doesn't support WebRTC");
// don't hang up the call: there could be other clients
// connected that do support WebRTC and declining the
// the call on their behalf would be really annoying.
return;
}
call.callId = content.call_id;
const initWithInvitePromise = call.initWithInvite(event);
this.calls.set(call.callId, call);
// if we stashed candidate events for that call ID, play them back now
if (this.candidateEventsByCall.get(call.callId)) {
for (const ev of this.candidateEventsByCall.get(call.callId)) {
call.onRemoteIceCandidatesReceived(ev);
}
}
// Were we trying to call that user (room)?
let existingCall;
for (const thisCall of this.calls.values()) {
const isCalling = [call_1.CallState.WaitLocalMedia, call_1.CallState.CreateOffer, call_1.CallState.InviteSent].includes(thisCall.state);
if (call.roomId === thisCall.roomId &&
thisCall.direction === call_1.CallDirection.Outbound &&
call.invitee === thisCall.invitee &&
isCalling) {
existingCall = thisCall;
break;
}
}
if (existingCall) {
// If we've only got to wait_local_media or create_offer and
// we've got an invite, pick the incoming call because we know
// we haven't sent our invite yet otherwise, pick whichever
// call has the lowest call ID (by string comparison)
if (existingCall.state === call_1.CallState.WaitLocalMedia ||
existingCall.state === call_1.CallState.CreateOffer ||
existingCall.callId > call.callId) {
logger_1.logger.log("Glare detected: answering incoming call " + call.callId +
" and canceling outgoing call " + existingCall.callId);
// Await init with invite as we need a peerConn for the following methods
yield initWithInvitePromise;
existingCall.replacedBy(call);
call.answer();
} }
else { else {
call.onRejectReceived(content); logger_1.logger.log("Glare detected: rejecting incoming call " + call.callId +
} " and keeping outgoing call " + existingCall.callId);
this.calls.delete(content.call_id); call.hangup(call_1.CallErrorCode.Replaced, true);
}
}
}
// The following events need a call
if (!call)
return;
// Ignore remote echo
if (event.getContent().party_id === call.ourPartyId)
return;
switch (type) {
case event_1.EventType.CallAnswer:
if (weSentTheEvent) {
if (call.state === call_1.CallState.Ringing) {
call.onAnsweredElsewhere(content);
} }
} }
else { else {
call.onAnswerReceived(event); initWithInvitePromise.then(() => {
this.client.emit("Call.incoming", call);
});
} }
break; }
case event_1.EventType.CallSelectAnswer: else if (type === event_1.EventType.CallCandidates) {
call.onSelectAnswerReceived(event); if (weSentTheEvent)
break; return;
case event_1.EventType.CallNegotiate: if (!call) {
call.onNegotiateReceived(event); // store the candidates; we may get a call eventually.
break; if (!this.candidateEventsByCall.has(content.call_id)) {
case event_1.EventType.CallAssertedIdentity: this.candidateEventsByCall.set(content.call_id, []);
case event_1.EventType.CallAssertedIdentityPrefix: }
call.onAssertedIdentityReceived(event); this.candidateEventsByCall.get(content.call_id).push(event);
break; }
case event_1.EventType.CallSDPStreamMetadataChanged: else {
case event_1.EventType.CallSDPStreamMetadataChangedPrefix: call.onRemoteIceCandidatesReceived(event);
call.onSDPStreamMetadataChangedReceived(event); }
break; }
} else if ([event_1.EventType.CallHangup, event_1.EventType.CallReject].includes(type)) {
// Note that we also observe our own hangups here so we can see
// if we've already rejected a call that would otherwise be valid
if (!call) {
// if not live, store the fact that the call has ended because
// we're probably getting events backwards so
// the hangup will come before the invite
call = call_1.createNewMatrixCall(this.client, event.getRoomId());
if (call) {
call.callId = content.call_id;
call.initWithHangup(event);
this.calls.set(content.call_id, call);
}
}
else {
if (call.state !== call_1.CallState.Ended) {
if (type === event_1.EventType.CallHangup) {
call.onHangupReceived(content);
}
else {
call.onRejectReceived(content);
}
this.calls.delete(content.call_id);
}
}
}
// The following events need a call
if (!call)
return;
// Ignore remote echo
if (event.getContent().party_id === call.ourPartyId)
return;
switch (type) {
case event_1.EventType.CallAnswer:
if (weSentTheEvent) {
if (call.state === call_1.CallState.Ringing) {
call.onAnsweredElsewhere(content);
}
}
else {
call.onAnswerReceived(event);
}
break;
case event_1.EventType.CallSelectAnswer:
call.onSelectAnswerReceived(event);
break;
case event_1.EventType.CallNegotiate:
call.onNegotiateReceived(event);
break;
case event_1.EventType.CallAssertedIdentity:
case event_1.EventType.CallAssertedIdentityPrefix:
call.onAssertedIdentityReceived(event);
break;
case event_1.EventType.CallSDPStreamMetadataChanged:
case event_1.EventType.CallSDPStreamMetadataChangedPrefix:
call.onSDPStreamMetadataChangedReceived(event);
break;
}
});
} }
} }
exports.CallEventHandler = CallEventHandler; exports.CallEventHandler = CallEventHandler;

File diff suppressed because one or more lines are too long

View File

@ -171,6 +171,9 @@ export class ConferenceCallManager extends EventEmitter {
this.localParticipant = null; this.localParticipant = null;
this.micMuted = false;
this.videoMuted = false;
this.client.on("RoomState.members", this._onRoomStateMembers); this.client.on("RoomState.members", this._onRoomStateMembers);
this.client.on("Call.incoming", this._onIncomingCall); this.client.on("Call.incoming", this._onIncomingCall);
this.callDebugger = new ConferenceCallDebugger(this); this.callDebugger = new ConferenceCallDebugger(this);
@ -307,12 +310,66 @@ 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.videoMuted = false;
clearTimeout(this._memberParticipantStateTimeout); clearTimeout(this._memberParticipantStateTimeout);
this.emit("participants_changed"); this.emit("participants_changed");
this.emit("left"); this.emit("left");
} }
setMicMuted(muted) {
this.micMuted = muted;
const localStream = this.client.localAVStream;
if (localStream) {
for (const track of localStream.getTracks()) {
if (track.kind === "audio") {
track.enabled = !this.micMuted;
}
}
}
for (let participant of this.participants) {
const call = participant.call;
if (
call &&
call.localUsermediaStream &&
call.isMicrophoneMuted() !== this.micMuted
) {
call.setMicrophoneMuted(this.micMuted);
}
}
}
setVideoMuted(muted) {
this.videoMuted = muted;
const localStream = this.client.localAVStream;
if (localStream) {
for (const track of localStream.getTracks()) {
if (track.kind === "video") {
track.enabled = !this.videoMuted;
}
}
}
for (let participant of this.participants) {
const call = participant.call;
if (
call &&
call.localUsermediaStream &&
call.isLocalVideoMuted() !== this.videoMuted
) {
call.setLocalVideoMuted(this.videoMuted);
}
}
}
logout() { logout() {
localStorage.removeItem("matrix-auth-store"); localStorage.removeItem("matrix-auth-store");
} }
@ -557,6 +614,20 @@ export class ConferenceCallManager extends EventEmitter {
*/ */
_onCallStateChanged = (participant, call, state) => { _onCallStateChanged = (participant, call, state) => {
if (
call.localUsermediaStream &&
call.isMicrophoneMuted() !== this.micMuted
) {
call.setMicrophoneMuted(this.micMuted);
}
if (
call.localUsermediaStream &&
call.isLocalVideoMuted() !== this.videoMuted
) {
call.setLocalVideoMuted(this.videoMuted);
}
this.emit("debugstate", participant.userId, call.callId, state); this.emit("debugstate", participant.userId, call.callId, state);
}; };

View File

@ -139,15 +139,28 @@ export function useConferenceCallManager(homeserverUrl) {
} }
export function useVideoRoom(manager, roomId, timeout = 5000) { export function useVideoRoom(manager, roomId, timeout = 5000) {
const [{ loading, joined, joining, room, participants, error }, setState] = const [
useState({ {
loading: true, loading,
joining: false, joined,
joined: false, joining,
room: undefined, room,
participants: [], participants,
error: undefined, error,
}); videoMuted,
micMuted,
},
setState,
] = useState({
loading: true,
joining: false,
joined: false,
room: undefined,
participants: [],
error: undefined,
videoMuted: false,
micMuted: false,
});
useEffect(() => { useEffect(() => {
setState((prevState) => ({ setState((prevState) => ({
@ -305,6 +318,16 @@ export function useVideoRoom(manager, roomId, timeout = 5000) {
}; };
}, [manager]); }, [manager]);
const toggleMuteMic = useCallback(() => {
manager.setMicMuted(!manager.micMuted);
setState((prevState) => ({ ...prevState, micMuted: manager.micMuted }));
}, [manager]);
const toggleMuteVideo = useCallback(() => {
manager.setVideoMuted(!manager.videoMuted);
setState((prevState) => ({ ...prevState, videoMuted: manager.videoMuted }));
}, [manager]);
return { return {
loading, loading,
joined, joined,
@ -314,6 +337,10 @@ export function useVideoRoom(manager, roomId, timeout = 5000) {
error, error,
joinCall, joinCall,
leaveCall, leaveCall,
toggleMuteVideo,
toggleMuteMic,
videoMuted,
micMuted,
}; };
} }

View File

@ -46,6 +46,10 @@ export function Room({ manager }) {
error, error,
joinCall, joinCall,
leaveCall, leaveCall,
toggleMuteVideo,
toggleMuteMic,
videoMuted,
micMuted,
} = 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");
@ -113,8 +117,8 @@ export function Room({ manager }) {
)} )}
{!loading && room && joined && ( {!loading && room && joined && (
<div className={styles.footer}> <div className={styles.footer}>
<MicButton /> <MicButton muted={micMuted} onClick={toggleMuteMic} />
<VideoButton /> <VideoButton enabled={videoMuted} onClick={toggleMuteVideo} />
<HangupButton onClick={leaveCall} /> <HangupButton onClick={leaveCall} />
</div> </div>
)} )}