dismiss a call when answered from another of our devices

pull/2039/head
Audric Ackermann 4 years ago
parent 7b0587876f
commit c1471426ac
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4

@ -17,6 +17,23 @@ export async function handleCallMessage(
const { type } = callMessage; const { type } = callMessage;
// we just allow self send of ANSWER message to remove the incoming call dialog when we accepted it from another device
if (
sender === UserUtils.getOurPubKeyStrFromCache() &&
callMessage.type !== SignalService.CallMessage.Type.ANSWER
) {
window.log.info('Dropping incoming call from ourself');
await removeFromCache(envelope);
return;
}
if (CallManager.isCallRejected(callMessage.uuid)) {
await removeFromCache(envelope);
window.log.info(`Dropping already rejected call ${callMessage.uuid}`);
return;
}
if (type === SignalService.CallMessage.Type.PROVISIONAL_ANSWER) { if (type === SignalService.CallMessage.Type.PROVISIONAL_ANSWER) {
await removeFromCache(envelope); await removeFromCache(envelope);

@ -1,13 +1,11 @@
import _ from 'lodash'; import _ from 'lodash';
import { getMessageById } from '../../data/data'; import { getMessageById } from '../../data/data';
import { MessageModel } from '../../models/message';
import { SignalService } from '../../protobuf'; import { SignalService } from '../../protobuf';
import { PnServer } from '../../pushnotification'; import { PnServer } from '../../pushnotification';
import { OpenGroupVisibleMessage } from '../messages/outgoing/visibleMessage/OpenGroupVisibleMessage'; import { OpenGroupVisibleMessage } from '../messages/outgoing/visibleMessage/OpenGroupVisibleMessage';
import { EncryptionType, RawMessage } from '../types'; import { EncryptionType, RawMessage } from '../types';
import { UserUtils } from '../utils'; import { UserUtils } from '../utils';
// tslint:disable-next-line no-unnecessary-class
export class MessageSentHandler { export class MessageSentHandler {
public static async handlePublicMessageSentSuccess( public static async handlePublicMessageSentSuccess(
sentMessage: OpenGroupVisibleMessage, sentMessage: OpenGroupVisibleMessage,
@ -54,10 +52,8 @@ export class MessageSentHandler {
let sentTo = fetchedMessage.get('sent_to') || []; let sentTo = fetchedMessage.get('sent_to') || [];
let isOurDevice = false; const isOurDevice = UserUtils.isUsFromCache(sentMessage.device);
if (sentMessage.device) {
isOurDevice = UserUtils.isUsFromCache(sentMessage.device);
}
// FIXME this is not correct and will cause issues with syncing // FIXME this is not correct and will cause issues with syncing
// At this point the only way to check for medium // At this point the only way to check for medium
// group is by comparing the encryption type // group is by comparing the encryption type
@ -113,8 +109,9 @@ export class MessageSentHandler {
window?.log?.warn( window?.log?.warn(
'Got an error while trying to sendSyncMessage(): fetchedMessage is null' 'Got an error while trying to sendSyncMessage(): fetchedMessage is null'
); );
return;
} }
fetchedMessage = tempFetchMessage as MessageModel; fetchedMessage = tempFetchMessage;
} catch (e) { } catch (e) {
window?.log?.warn('Got an error while trying to sendSyncMessage():', e); window?.log?.warn('Got an error while trying to sendSyncMessage():', e);
} }

@ -318,7 +318,6 @@ export class SwarmPolling {
} }
private loadGroupIds() { private loadGroupIds() {
// Start polling for medium size groups as well (they might be in different swarms)
const convos = getConversationController().getConversations(); const convos = getConversationController().getConversations();
const mediumGroupsOnly = convos.filter( const mediumGroupsOnly = convos.filter(
@ -328,7 +327,6 @@ export class SwarmPolling {
mediumGroupsOnly.forEach((c: any) => { mediumGroupsOnly.forEach((c: any) => {
this.addGroupId(new PubKey(c.id)); this.addGroupId(new PubKey(c.id));
// TODO: unsubscribe if the group is deleted
}); });
} }

@ -1,5 +1,5 @@
import _ from 'lodash'; import _ from 'lodash';
import { MessageUtils, ToastUtils } from '.'; import { MessageUtils, ToastUtils, UserUtils } from '.';
import { getCallMediaPermissionsSettings } from '../../components/session/settings/SessionSettings'; import { getCallMediaPermissionsSettings } from '../../components/session/settings/SessionSettings';
import { getConversationById } from '../../data/data'; import { getConversationById } from '../../data/data';
import { ConversationModel } from '../../models/conversation'; import { ConversationModel } from '../../models/conversation';
@ -28,6 +28,8 @@ export type InputItem = { deviceId: string; label: string };
let currentCallUUID: string | undefined; let currentCallUUID: string | undefined;
const rejectedCallUUIDS: Set<string> = new Set();
export type CallManagerOptionsType = { export type CallManagerOptionsType = {
localStream: MediaStream | null; localStream: MediaStream | null;
remoteStream: MediaStream | null; remoteStream: MediaStream | null;
@ -80,7 +82,7 @@ export function removeVideoEventsListener(uniqueId: string) {
} }
/** /**
* This field stores all the details received about a specific call with the same uuid. It is a per pubkey and per device cache. * This field stores all the details received about a specific call with the same uuid. It is a per pubkey and per call cache.
*/ */
const callCache = new Map<string, Map<string, Array<SignalService.CallMessage>>>(); const callCache = new Map<string, Map<string, Array<SignalService.CallMessage>>>();
@ -203,7 +205,15 @@ export async function selectCameraByDeviceId(cameraDeviceId: string) {
if (!peerConnection) { if (!peerConnection) {
throw new Error('cannot selectCameraByDeviceId without a peer connection'); throw new Error('cannot selectCameraByDeviceId without a peer connection');
} }
const sender = peerConnection.getSenders().find(s => { let sender = peerConnection.getSenders().find(s => {
return s.track?.kind === videoTrack.kind;
});
// video might be completely off
if (!sender) {
peerConnection.addTrack(videoTrack);
}
sender = peerConnection.getSenders().find(s => {
return s.track?.kind === videoTrack.kind; return s.track?.kind === videoTrack.kind;
}); });
if (sender) { if (sender) {
@ -217,8 +227,6 @@ export async function selectCameraByDeviceId(cameraDeviceId: string) {
sendVideoStatusViaDataChannel(); sendVideoStatusViaDataChannel();
callVideoListeners(); callVideoListeners();
} else {
throw new Error('Failed to get sender for selectCameraByDeviceId ');
} }
} catch (e) { } catch (e) {
window.log.warn('selectCameraByDeviceId failed with', e.message); window.log.warn('selectCameraByDeviceId failed with', e.message);
@ -313,7 +321,7 @@ async function handleNegotiationNeededEvent(recipient: string) {
uuid: currentCallUUID, uuid: currentCallUUID,
}); });
window.log.info('sending OFFER MESSAGE'); window.log.info(`sending OFFER MESSAGE with callUUID: ${currentCallUUID}`);
const negotationOfferSendResult = await getMessageQueue().sendToPubKeyNonDurably( const negotationOfferSendResult = await getMessageQueue().sendToPubKeyNonDurably(
PubKey.cast(recipient), PubKey.cast(recipient),
offerMessage offerMessage
@ -340,10 +348,7 @@ function handleIceCandidates(event: RTCPeerConnectionIceEvent, pubkey: string) {
async function openMediaDevicesAndAddTracks() { async function openMediaDevicesAndAddTracks() {
try { try {
await updateConnectedDevices(); await updateConnectedDevices();
if (!camerasList.length) {
ToastUtils.pushNoCameraFound();
return;
}
if (!audioInputsList.length) { if (!audioInputsList.length) {
ToastUtils.pushNoAudioInputFound(); ToastUtils.pushNoAudioInputFound();
return; return;
@ -352,34 +357,32 @@ async function openMediaDevicesAndAddTracks() {
selectedAudioInputId = audioInputsList[0].deviceId; selectedAudioInputId = audioInputsList[0].deviceId;
selectedCameraId = DEVICE_DISABLED_DEVICE_ID; selectedCameraId = DEVICE_DISABLED_DEVICE_ID;
window.log.info( window.log.info(
`openMediaDevices videoDevice:${selectedCameraId}:${camerasList[0].label} audioDevice:${selectedAudioInputId}` `openMediaDevices videoDevice:${selectedCameraId} audioDevice:${selectedAudioInputId}`
); );
const devicesConfig = { const devicesConfig = {
audio: { audio: {
deviceId: selectedAudioInputId, deviceId: { exact: selectedAudioInputId },
echoCancellation: true, echoCancellation: true,
}, },
video: { // we don't need a video stream on start
deviceId: selectedCameraId, video: false,
// width: VIDEO_WIDTH,
// height: Math.floor(VIDEO_WIDTH * VIDEO_RATIO),
},
}; };
mediaDevices = await navigator.mediaDevices.getUserMedia(devicesConfig); mediaDevices = await navigator.mediaDevices.getUserMedia(devicesConfig);
mediaDevices.getTracks().map(track => { mediaDevices.getTracks().map(track => {
if (track.kind === 'video') { // if (track.kind === 'video') {
track.enabled = false; // track.enabled = false;
} // }
if (mediaDevices) { if (mediaDevices) {
peerConnection?.addTrack(track, mediaDevices); peerConnection?.addTrack(track, mediaDevices);
} }
}); });
} catch (err) { } catch (err) {
window.log.warn('openMediaDevices: ', err);
ToastUtils.pushVideoCallPermissionNeeded(); ToastUtils.pushVideoCallPermissionNeeded();
closeVideoCall(); await closeVideoCall();
} }
callVideoListeners(); callVideoListeners();
} }
@ -412,6 +415,9 @@ export async function USER_callRecipient(recipient: string) {
}); });
window.log.info('Sending preOffer message to ', ed25519Str(recipient)); window.log.info('Sending preOffer message to ', ed25519Str(recipient));
// we do it manually as the sendToPubkeyNonDurably rely on having a message saved to the db for MessageSentSuccess
// which is not the case for a pre offer message (the message only exists in memory)
const rawMessage = await MessageUtils.toRawMessage(PubKey.cast(recipient), preOfferMsg); const rawMessage = await MessageUtils.toRawMessage(PubKey.cast(recipient), preOfferMsg);
const { wrappedEnvelope } = await MessageSender.send(rawMessage); const { wrappedEnvelope } = await MessageSender.send(rawMessage);
void PnServer.notifyPnServer(wrappedEnvelope, recipient); void PnServer.notifyPnServer(wrappedEnvelope, recipient);
@ -455,7 +461,9 @@ const iceSenderDebouncer = _.debounce(async (recipient: string) => {
uuid: currentCallUUID, uuid: currentCallUUID,
}); });
window.log.info('sending ICE CANDIDATES MESSAGE to ', recipient); window.log.info(
`sending ICE CANDIDATES MESSAGE to ${ed25519Str(recipient)} about call ${currentCallUUID}`
);
await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(recipient), callIceCandicates); await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(recipient), callIceCandicates);
}, 2000); }, 2000);
@ -479,7 +487,7 @@ const findLastMessageTypeFromSender = (sender: string, msgType: SignalService.Ca
function handleSignalingStateChangeEvent() { function handleSignalingStateChangeEvent() {
if (peerConnection?.signalingState === 'closed') { if (peerConnection?.signalingState === 'closed') {
closeVideoCall(); void closeVideoCall();
} }
} }
@ -487,14 +495,14 @@ function handleConnectionStateChanged(pubkey: string) {
window.log.info('handleConnectionStateChanged :', peerConnection?.connectionState); window.log.info('handleConnectionStateChanged :', peerConnection?.connectionState);
if (peerConnection?.signalingState === 'closed' || peerConnection?.connectionState === 'failed') { if (peerConnection?.signalingState === 'closed' || peerConnection?.connectionState === 'failed') {
closeVideoCall(); void closeVideoCall();
} else if (peerConnection?.connectionState === 'connected') { } else if (peerConnection?.connectionState === 'connected') {
setIsRinging(false); setIsRinging(false);
window.inboxStore?.dispatch(callConnected({ pubkey })); window.inboxStore?.dispatch(callConnected({ pubkey }));
} }
} }
function closeVideoCall() { async function closeVideoCall() {
window.log.info('closingVideoCall '); window.log.info('closingVideoCall ');
setIsRinging(false); setIsRinging(false);
if (peerConnection) { if (peerConnection) {
@ -533,6 +541,18 @@ function closeVideoCall() {
currentCallUUID = undefined; currentCallUUID = undefined;
window.inboxStore?.dispatch(setFullScreenCall(false)); window.inboxStore?.dispatch(setFullScreenCall(false));
const convos = getConversationController().getConversations();
const callingConvos = convos.filter(convo => convo.callState !== undefined);
if (callingConvos.length > 0) {
// reset all convos callState
await Promise.all(
callingConvos.map(async m => {
m.callState = undefined;
await m.commit();
})
);
}
remoteVideoStreamIsMuted = true; remoteVideoStreamIsMuted = true;
makingOffer = false; makingOffer = false;
@ -712,7 +732,7 @@ export async function USER_acceptIncomingCallRequest(fromSender: string) {
} }
// tslint:disable-next-line: function-name // tslint:disable-next-line: function-name
export async function USER_rejectIncomingCallRequest(fromSender: string) { export async function USER_rejectIncomingCallRequest(fromSender: string, forcedUUID?: string) {
setIsRinging(false); setIsRinging(false);
const lastOfferMessage = findLastMessageTypeFromSender( const lastOfferMessage = findLastMessageTypeFromSender(
@ -720,32 +740,36 @@ export async function USER_rejectIncomingCallRequest(fromSender: string) {
SignalService.CallMessage.Type.OFFER SignalService.CallMessage.Type.OFFER
); );
const lastCallUUID = lastOfferMessage?.uuid; const aboutCallUUID = forcedUUID || lastOfferMessage?.uuid;
window.log.info(`USER_rejectIncomingCallRequest ${ed25519Str(fromSender)}: ${lastCallUUID}`); window.log.info(`USER_rejectIncomingCallRequest ${ed25519Str(fromSender)}: ${aboutCallUUID}`);
if (lastCallUUID) { if (aboutCallUUID) {
rejectedCallUUIDS.add(aboutCallUUID);
const endCallMessage = new CallMessage({ const endCallMessage = new CallMessage({
type: SignalService.CallMessage.Type.END_CALL, type: SignalService.CallMessage.Type.END_CALL,
timestamp: Date.now(), timestamp: Date.now(),
uuid: lastCallUUID, uuid: aboutCallUUID,
}); });
await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(fromSender), endCallMessage); await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(fromSender), endCallMessage);
// delete all msg not from that uuid only but from that sender pubkey // delete all msg not from that uuid only but from that sender pubkey
clearCallCacheFromPubkeyAndUUID(fromSender, lastCallUUID); clearCallCacheFromPubkeyAndUUID(fromSender, aboutCallUUID);
} }
window.inboxStore?.dispatch( // if we got a forceUUID, it means we just to deny another user's device incoming call we are already in a call with.
endCall({ if (!forcedUUID) {
pubkey: fromSender, window.inboxStore?.dispatch(
}) endCall({
); pubkey: fromSender,
})
);
const convos = getConversationController().getConversations(); const convos = getConversationController().getConversations();
const callingConvos = convos.filter(convo => convo.callState !== undefined); const callingConvos = convos.filter(convo => convo.callState !== undefined);
if (callingConvos.length > 0) { if (callingConvos.length > 0) {
// we just got a new offer from someone we are already in a call with // we just got a new offer from someone we are already in a call with
if (callingConvos.length === 1 && callingConvos[0].id === fromSender) { if (callingConvos.length === 1 && callingConvos[0].id === fromSender) {
closeVideoCall(); await closeVideoCall();
}
} }
} }
} }
@ -758,6 +782,7 @@ export async function USER_hangup(fromSender: string) {
window.log.warn('should not be able to hangup without a currentCallUUID'); window.log.warn('should not be able to hangup without a currentCallUUID');
return; return;
} else { } else {
rejectedCallUUIDS.add(currentCallUUID);
const endCallMessage = new CallMessage({ const endCallMessage = new CallMessage({
type: SignalService.CallMessage.Type.END_CALL, type: SignalService.CallMessage.Type.END_CALL,
timestamp: Date.now(), timestamp: Date.now(),
@ -773,17 +798,19 @@ export async function USER_hangup(fromSender: string) {
clearCallCacheFromPubkeyAndUUID(fromSender, currentCallUUID); clearCallCacheFromPubkeyAndUUID(fromSender, currentCallUUID);
closeVideoCall(); await closeVideoCall();
} }
export function handleCallTypeEndCall(sender: string, aboutCallUUID?: string) { export function handleCallTypeEndCall(sender: string, aboutCallUUID?: string) {
window.log.info('handling callMessage END_CALL:', aboutCallUUID); window.log.info('handling callMessage END_CALL:', aboutCallUUID);
if (aboutCallUUID) { if (aboutCallUUID) {
rejectedCallUUIDS.add(aboutCallUUID);
clearCallCacheFromPubkeyAndUUID(sender, aboutCallUUID); clearCallCacheFromPubkeyAndUUID(sender, aboutCallUUID);
if (aboutCallUUID === currentCallUUID) { if (aboutCallUUID === currentCallUUID) {
closeVideoCall(); void closeVideoCall();
window.inboxStore?.dispatch(endCall({ pubkey: sender })); window.inboxStore?.dispatch(endCall({ pubkey: sender }));
} }
@ -817,9 +844,17 @@ async function buildAnswerAndSendIt(sender: string) {
window.log.info('sending ANSWER MESSAGE'); window.log.info('sending ANSWER MESSAGE');
await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(sender), callAnswerMessage); await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(sender), callAnswerMessage);
await getMessageQueue().sendToPubKeyNonDurably(
UserUtils.getOurPubKeyFromCache(),
callAnswerMessage
);
} }
} }
export function isCallRejected(uuid: string) {
return rejectedCallUUIDS.has(uuid);
}
export async function handleCallTypeOffer( export async function handleCallTypeOffer(
sender: string, sender: string,
callMessage: SignalService.CallMessage, callMessage: SignalService.CallMessage,
@ -832,20 +867,22 @@ export async function handleCallTypeOffer(
} }
window.log.info('handling callMessage OFFER with uuid: ', remoteCallUUID); window.log.info('handling callMessage OFFER with uuid: ', remoteCallUUID);
const convos = getConversationController().getConversations();
const callingConvos = convos.filter(convo => convo.callState !== undefined);
if (!getCallMediaPermissionsSettings()) { if (!getCallMediaPermissionsSettings()) {
await handleMissedCall(sender, incomingOfferTimestamp, true); await handleMissedCall(sender, incomingOfferTimestamp, true);
return; return;
} }
if (callingConvos.length > 0) { if (currentCallUUID && currentCallUUID !== remoteCallUUID) {
// we just got a new offer from someone we are NOT already in a call with // we just got a new offer with a different callUUID. this is a missed call (from either the same sender or another one)
if (callingConvos.length !== 1 || callingConvos[0].id !== sender) { if (callCache.get(sender)?.has(currentCallUUID)) {
await handleMissedCall(sender, incomingOfferTimestamp, false); // this is a missed call from the same sender (another call from another device maybe?)
// just reject it.
await USER_rejectIncomingCallRequest(sender, remoteCallUUID);
return; return;
} }
await handleMissedCall(sender, incomingOfferTimestamp, false);
return;
} }
const readyForOffer = const readyForOffer =
@ -859,7 +896,7 @@ export async function handleCallTypeOffer(
return; return;
} }
if (callingConvos.length === 1 && callingConvos[0].id === sender) { if (remoteCallUUID === currentCallUUID && currentCallUUID) {
window.log.info('Got a new offer message from our ongoing call'); window.log.info('Got a new offer message from our ongoing call');
isSettingRemoteAnswerPending = false; isSettingRemoteAnswerPending = false;
const remoteDesc = new RTCSessionDescription({ const remoteDesc = new RTCSessionDescription({
@ -938,7 +975,48 @@ export async function handleCallTypeAnswer(sender: string, callMessage: SignalSe
return; return;
} }
window.log.info('handling callMessage ANSWER'); // this is an answer we sent to ourself, this must be about another of our device accepting an incoming call
// if we accepted that call already from the current device, currentCallUUID is set
if (sender === UserUtils.getOurPubKeyStrFromCache() && remoteCallUUID !== currentCallUUID) {
window.log.info(`handling callMessage ANSWER from ourself about call ${remoteCallUUID}`);
let foundOwnerOfCallUUID: string | undefined;
for (const deviceKey of callCache.keys()) {
if (foundOwnerOfCallUUID) {
break;
}
for (const callUUIDEntry of callCache.get(deviceKey) as Map<
string,
Array<SignalService.CallMessage>
>) {
if (callUUIDEntry[0] === remoteCallUUID) {
foundOwnerOfCallUUID = deviceKey;
break;
}
}
}
if (foundOwnerOfCallUUID) {
rejectedCallUUIDS.add(remoteCallUUID);
const convos = getConversationController().getConversations();
const callingConvos = convos.filter(convo => convo.callState !== undefined);
if (callingConvos.length > 0) {
// we just got a new offer from someone we are already in a call with
if (callingConvos.length === 1 && callingConvos[0].id === foundOwnerOfCallUUID) {
await closeVideoCall();
}
}
window.inboxStore?.dispatch(
endCall({
pubkey: foundOwnerOfCallUUID,
})
);
return;
}
} else {
window.log.info(`handling callMessage ANSWER from ${remoteCallUUID}`);
}
pushCallMessageToCallCache(sender, remoteCallUUID, callMessage); pushCallMessageToCallCache(sender, remoteCallUUID, callMessage);
@ -946,8 +1024,15 @@ export async function handleCallTypeAnswer(sender: string, callMessage: SignalSe
window.log.info('handleCallTypeAnswer without peer connection. Dropping'); window.log.info('handleCallTypeAnswer without peer connection. Dropping');
return; return;
} }
window.inboxStore?.dispatch(answerCall({ pubkey: sender })); window.inboxStore?.dispatch(
const remoteDesc = new RTCSessionDescription({ type: 'answer', sdp: callMessage.sdps[0] }); answerCall({
pubkey: sender,
})
);
const remoteDesc = new RTCSessionDescription({
type: 'answer',
sdp: callMessage.sdps[0],
});
// window.log?.info('Setting remote answer pending'); // window.log?.info('Setting remote answer pending');
isSettingRemoteAnswerPending = true; isSettingRemoteAnswerPending = true;

Loading…
Cancel
Save