2020-10-30 20:34:04 +00:00
|
|
|
// Copyright 2020 Signal Messenger, LLC
|
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
2020-09-03 14:59:24 +00:00
|
|
|
/* eslint-disable class-methods-use-this */
|
|
|
|
|
2020-06-04 18:16:19 +00:00
|
|
|
import {
|
|
|
|
Call,
|
|
|
|
CallEndedReason,
|
|
|
|
CallId,
|
2020-09-28 19:02:35 +00:00
|
|
|
CallingMessage,
|
2020-06-04 18:16:19 +00:00
|
|
|
CallLogLevel,
|
|
|
|
CallSettings,
|
|
|
|
CallState,
|
2020-08-27 00:03:42 +00:00
|
|
|
CanvasVideoRenderer,
|
2020-11-13 19:57:55 +00:00
|
|
|
ConnectionState,
|
|
|
|
JoinState,
|
|
|
|
HttpMethod,
|
2020-06-04 18:16:19 +00:00
|
|
|
DeviceId,
|
2020-11-13 19:57:55 +00:00
|
|
|
GroupCall,
|
|
|
|
GroupMemberInfo,
|
2020-08-27 00:03:42 +00:00
|
|
|
GumVideoCapturer,
|
2020-09-28 19:02:35 +00:00
|
|
|
HangupMessage,
|
|
|
|
HangupType,
|
|
|
|
OfferType,
|
2020-11-13 19:57:55 +00:00
|
|
|
OpaqueMessage,
|
2020-06-04 18:16:19 +00:00
|
|
|
RingRTC,
|
|
|
|
UserId,
|
2020-11-13 19:57:55 +00:00
|
|
|
VideoFrameSource,
|
2020-06-04 18:16:19 +00:00
|
|
|
} from 'ringrtc';
|
2020-11-13 19:57:55 +00:00
|
|
|
import { uniqBy, noop } from 'lodash';
|
2020-09-12 00:53:19 +00:00
|
|
|
|
2020-11-06 17:36:37 +00:00
|
|
|
import { ActionsType as UxActionsType } from '../state/ducks/calling';
|
2020-11-13 19:57:55 +00:00
|
|
|
import { getConversationCallMode } from '../state/ducks/conversations';
|
2020-09-28 19:02:35 +00:00
|
|
|
import { EnvelopeClass } from '../textsecure.d';
|
2020-11-13 19:57:55 +00:00
|
|
|
import {
|
|
|
|
CallMode,
|
|
|
|
AudioDevice,
|
|
|
|
MediaDeviceSettings,
|
|
|
|
GroupCallConnectionState,
|
|
|
|
GroupCallJoinState,
|
|
|
|
} from '../types/Calling';
|
2020-09-24 20:57:54 +00:00
|
|
|
import { ConversationModel } from '../models/conversations';
|
2020-11-13 19:57:55 +00:00
|
|
|
import {
|
|
|
|
base64ToArrayBuffer,
|
|
|
|
uuidToArrayBuffer,
|
|
|
|
arrayBufferToUuid,
|
|
|
|
} from '../Crypto';
|
|
|
|
import { getOwn } from '../util/getOwn';
|
|
|
|
import { fetchMembershipProof, getMembershipList } from '../groups';
|
|
|
|
import { missingCaseError } from '../util/missingCaseError';
|
|
|
|
|
|
|
|
const RINGRTC_HTTP_METHOD_TO_OUR_HTTP_METHOD: Map<
|
|
|
|
HttpMethod,
|
|
|
|
'GET' | 'PUT' | 'POST'
|
|
|
|
> = new Map([
|
|
|
|
[HttpMethod.Get, 'GET'],
|
|
|
|
[HttpMethod.Put, 'PUT'],
|
|
|
|
[HttpMethod.Post, 'POST'],
|
|
|
|
]);
|
2020-06-04 18:16:19 +00:00
|
|
|
|
|
|
|
export {
|
|
|
|
CallState,
|
|
|
|
CanvasVideoRenderer,
|
|
|
|
GumVideoCapturer,
|
|
|
|
VideoCapturer,
|
|
|
|
VideoRenderer,
|
|
|
|
} from 'ringrtc';
|
|
|
|
|
|
|
|
export class CallingClass {
|
2020-08-27 00:03:42 +00:00
|
|
|
readonly videoCapturer: GumVideoCapturer;
|
2020-09-03 14:59:24 +00:00
|
|
|
|
2020-08-27 00:03:42 +00:00
|
|
|
readonly videoRenderer: CanvasVideoRenderer;
|
2020-09-03 14:59:24 +00:00
|
|
|
|
2020-06-04 18:16:19 +00:00
|
|
|
private uxActions?: UxActionsType;
|
2020-09-03 14:59:24 +00:00
|
|
|
|
2020-08-27 00:03:42 +00:00
|
|
|
private lastMediaDeviceSettings?: MediaDeviceSettings;
|
2020-09-03 14:59:24 +00:00
|
|
|
|
2020-08-27 00:03:42 +00:00
|
|
|
private deviceReselectionTimer?: NodeJS.Timeout;
|
|
|
|
|
2020-11-13 19:57:55 +00:00
|
|
|
private callsByConversation: { [conversationId: string]: Call | GroupCall };
|
2020-11-06 17:36:37 +00:00
|
|
|
|
2020-08-27 00:03:42 +00:00
|
|
|
constructor() {
|
|
|
|
this.videoCapturer = new GumVideoCapturer(640, 480, 30);
|
|
|
|
this.videoRenderer = new CanvasVideoRenderer();
|
2020-11-06 17:36:37 +00:00
|
|
|
|
|
|
|
this.callsByConversation = {};
|
2020-08-27 00:03:42 +00:00
|
|
|
}
|
2020-06-04 18:16:19 +00:00
|
|
|
|
|
|
|
initialize(uxActions: UxActionsType): void {
|
|
|
|
this.uxActions = uxActions;
|
|
|
|
if (!uxActions) {
|
|
|
|
throw new Error('CallingClass.initialize: Invalid uxActions.');
|
|
|
|
}
|
|
|
|
RingRTC.handleOutgoingSignaling = this.handleOutgoingSignaling.bind(this);
|
|
|
|
RingRTC.handleIncomingCall = this.handleIncomingCall.bind(this);
|
|
|
|
RingRTC.handleAutoEndedIncomingCallRequest = this.handleAutoEndedIncomingCallRequest.bind(
|
|
|
|
this
|
|
|
|
);
|
|
|
|
RingRTC.handleLogMessage = this.handleLogMessage.bind(this);
|
2020-11-13 19:57:55 +00:00
|
|
|
RingRTC.handleSendHttpRequest = this.handleSendHttpRequest.bind(this);
|
|
|
|
RingRTC.handleSendCallMessage = this.handleSendCallMessage.bind(this);
|
2020-06-04 18:16:19 +00:00
|
|
|
}
|
|
|
|
|
2020-10-08 01:25:33 +00:00
|
|
|
async startCallingLobby(
|
2020-09-24 20:57:54 +00:00
|
|
|
conversation: ConversationModel,
|
2020-06-04 18:16:19 +00:00
|
|
|
isVideoCall: boolean
|
2020-09-03 14:59:24 +00:00
|
|
|
): Promise<void> {
|
2020-10-08 01:25:33 +00:00
|
|
|
window.log.info('CallingClass.startCallingLobby()');
|
2020-09-25 21:05:25 +00:00
|
|
|
|
2020-11-13 19:57:55 +00:00
|
|
|
const conversationProps = conversation.format();
|
|
|
|
const callMode = getConversationCallMode(conversationProps);
|
|
|
|
switch (callMode) {
|
|
|
|
case CallMode.None:
|
|
|
|
window.log.error(
|
|
|
|
'Conversation does not support calls, new call not allowed.'
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
case CallMode.Direct:
|
|
|
|
if (!this.getRemoteUserIdFromConversation(conversation)) {
|
|
|
|
window.log.error(
|
|
|
|
'Missing remote user identifier, new call not allowed.'
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case CallMode.Group:
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
throw missingCaseError(callMode);
|
|
|
|
}
|
|
|
|
|
2020-06-04 18:16:19 +00:00
|
|
|
if (!this.uxActions) {
|
|
|
|
window.log.error('Missing uxActions, new call not allowed.');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-11-13 19:57:55 +00:00
|
|
|
if (!this.localDeviceId) {
|
|
|
|
window.log.error(
|
|
|
|
'Missing local device identifier, new call not allowed.'
|
|
|
|
);
|
2020-06-04 18:16:19 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const haveMediaPermissions = await this.requestPermissions(isVideoCall);
|
|
|
|
if (!haveMediaPermissions) {
|
|
|
|
window.log.info('Permissions were denied, new call not allowed.');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-11-13 19:57:55 +00:00
|
|
|
window.log.info('CallingClass.startCallingLobby(): Starting lobby');
|
2020-10-08 01:25:33 +00:00
|
|
|
|
2020-11-13 19:57:55 +00:00
|
|
|
switch (callMode) {
|
|
|
|
case CallMode.Direct:
|
|
|
|
this.uxActions.showCallLobby({
|
|
|
|
callMode: CallMode.Direct,
|
|
|
|
conversationId: conversationProps.id,
|
|
|
|
hasLocalAudio: true,
|
|
|
|
hasLocalVideo: isVideoCall,
|
|
|
|
});
|
|
|
|
break;
|
|
|
|
case CallMode.Group: {
|
|
|
|
if (
|
|
|
|
!conversationProps.groupId ||
|
|
|
|
!conversationProps.publicParams ||
|
|
|
|
!conversationProps.secretParams
|
|
|
|
) {
|
|
|
|
window.log.error(
|
|
|
|
'Conversation is missing required parameters. Cannot connect group call'
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const groupCall = this.connectGroupCall(conversationProps.id, {
|
|
|
|
groupId: conversationProps.groupId,
|
|
|
|
publicParams: conversationProps.publicParams,
|
|
|
|
secretParams: conversationProps.secretParams,
|
|
|
|
});
|
|
|
|
|
|
|
|
groupCall.setOutgoingAudioMuted(false);
|
|
|
|
groupCall.setOutgoingVideoMuted(!isVideoCall);
|
|
|
|
|
|
|
|
this.uxActions.showCallLobby({
|
|
|
|
callMode: CallMode.Group,
|
|
|
|
conversationId: conversationProps.id,
|
|
|
|
...this.formatGroupCallForRedux(groupCall),
|
|
|
|
});
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
throw missingCaseError(callMode);
|
2020-10-08 01:25:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
await this.startDeviceReselectionTimer();
|
2020-11-03 00:47:18 +00:00
|
|
|
|
|
|
|
if (isVideoCall) {
|
|
|
|
this.enableLocalCamera();
|
|
|
|
}
|
2020-10-08 01:25:33 +00:00
|
|
|
}
|
|
|
|
|
2020-11-13 19:57:55 +00:00
|
|
|
stopCallingLobby(conversationId?: string): void {
|
2020-10-08 01:25:33 +00:00
|
|
|
this.disableLocalCamera();
|
|
|
|
this.stopDeviceReselectionTimer();
|
|
|
|
this.lastMediaDeviceSettings = undefined;
|
2020-11-13 19:57:55 +00:00
|
|
|
|
|
|
|
if (conversationId) {
|
|
|
|
this.getGroupCall(conversationId)?.disconnect();
|
|
|
|
}
|
2020-10-08 01:25:33 +00:00
|
|
|
}
|
|
|
|
|
2020-11-13 19:57:55 +00:00
|
|
|
async startOutgoingDirectCall(
|
2020-10-08 01:25:33 +00:00
|
|
|
conversationId: string,
|
2020-11-06 17:36:37 +00:00
|
|
|
hasLocalAudio: boolean,
|
|
|
|
hasLocalVideo: boolean
|
2020-10-08 01:25:33 +00:00
|
|
|
): Promise<void> {
|
2020-11-13 19:57:55 +00:00
|
|
|
window.log.info('CallingClass.startOutgoingDirectCall()');
|
2020-10-08 01:25:33 +00:00
|
|
|
|
|
|
|
if (!this.uxActions) {
|
|
|
|
throw new Error('Redux actions not available');
|
|
|
|
}
|
|
|
|
|
|
|
|
const conversation = window.ConversationController.get(conversationId);
|
|
|
|
|
|
|
|
if (!conversation) {
|
|
|
|
window.log.error('Could not find conversation, cannot start call');
|
|
|
|
this.stopCallingLobby();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const remoteUserId = this.getRemoteUserIdFromConversation(conversation);
|
|
|
|
if (!remoteUserId || !this.localDeviceId) {
|
|
|
|
window.log.error('Missing identifier, new call not allowed.');
|
|
|
|
this.stopCallingLobby();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-11-06 17:36:37 +00:00
|
|
|
const haveMediaPermissions = await this.requestPermissions(hasLocalVideo);
|
2020-10-08 01:25:33 +00:00
|
|
|
if (!haveMediaPermissions) {
|
|
|
|
window.log.info('Permissions were denied, new call not allowed.');
|
|
|
|
this.stopCallingLobby();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-11-13 19:57:55 +00:00
|
|
|
window.log.info(
|
|
|
|
'CallingClass.startOutgoingDirectCall(): Getting call settings'
|
|
|
|
);
|
2020-09-25 21:05:25 +00:00
|
|
|
|
2020-08-27 00:03:42 +00:00
|
|
|
const callSettings = await this.getCallSettings(conversation);
|
|
|
|
|
|
|
|
// Check state after awaiting to debounce call button.
|
|
|
|
if (RingRTC.call && RingRTC.call.state !== CallState.Ended) {
|
|
|
|
window.log.info('Call already in progress, new call not allowed.');
|
2020-10-08 01:25:33 +00:00
|
|
|
this.stopCallingLobby();
|
2020-08-27 00:03:42 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-11-13 19:57:55 +00:00
|
|
|
window.log.info(
|
|
|
|
'CallingClass.startOutgoingDirectCall(): Starting in RingRTC'
|
|
|
|
);
|
2020-09-25 21:05:25 +00:00
|
|
|
|
2020-06-04 18:16:19 +00:00
|
|
|
// We could make this faster by getting the call object
|
|
|
|
// from the RingRTC before we lookup the ICE servers.
|
|
|
|
const call = RingRTC.startOutgoingCall(
|
|
|
|
remoteUserId,
|
2020-11-06 17:36:37 +00:00
|
|
|
hasLocalVideo,
|
2020-06-04 18:16:19 +00:00
|
|
|
this.localDeviceId,
|
2020-08-27 00:03:42 +00:00
|
|
|
callSettings
|
2020-06-04 18:16:19 +00:00
|
|
|
);
|
|
|
|
|
2020-11-06 17:36:37 +00:00
|
|
|
RingRTC.setOutgoingAudio(call.callId, hasLocalAudio);
|
2020-08-27 00:03:42 +00:00
|
|
|
RingRTC.setVideoCapturer(call.callId, this.videoCapturer);
|
|
|
|
RingRTC.setVideoRenderer(call.callId, this.videoRenderer);
|
2020-06-04 18:16:19 +00:00
|
|
|
this.attachToCall(conversation, call);
|
|
|
|
|
|
|
|
this.uxActions.outgoingCall({
|
2020-11-06 17:36:37 +00:00
|
|
|
conversationId: conversation.id,
|
|
|
|
hasLocalAudio,
|
|
|
|
hasLocalVideo,
|
2020-06-04 18:16:19 +00:00
|
|
|
});
|
2020-11-06 17:36:37 +00:00
|
|
|
|
|
|
|
await this.startDeviceReselectionTimer();
|
|
|
|
}
|
|
|
|
|
2020-11-13 19:57:55 +00:00
|
|
|
private getDirectCall(conversationId: string): undefined | Call {
|
|
|
|
const call = getOwn(this.callsByConversation, conversationId);
|
|
|
|
return call instanceof Call ? call : undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
private getGroupCall(conversationId: string): undefined | GroupCall {
|
|
|
|
const call = getOwn(this.callsByConversation, conversationId);
|
|
|
|
return call instanceof GroupCall ? call : undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Connect to a conversation's group call and connect it to Redux.
|
|
|
|
*
|
|
|
|
* Should only be called with group call-compatible conversations.
|
|
|
|
*
|
|
|
|
* Idempotent.
|
|
|
|
*/
|
|
|
|
connectGroupCall(
|
|
|
|
conversationId: string,
|
|
|
|
{
|
|
|
|
groupId,
|
|
|
|
publicParams,
|
|
|
|
secretParams,
|
|
|
|
}: {
|
|
|
|
groupId: string;
|
|
|
|
publicParams: string;
|
|
|
|
secretParams: string;
|
|
|
|
}
|
|
|
|
): GroupCall {
|
|
|
|
const existing = this.getGroupCall(conversationId);
|
|
|
|
if (existing) {
|
|
|
|
const isExistingCallNotConnected =
|
|
|
|
existing.getLocalDeviceState().connectionState ===
|
|
|
|
ConnectionState.NotConnected;
|
|
|
|
if (isExistingCallNotConnected) {
|
|
|
|
existing.connect();
|
|
|
|
}
|
|
|
|
return existing;
|
|
|
|
}
|
|
|
|
|
|
|
|
const groupIdBuffer = base64ToArrayBuffer(groupId);
|
|
|
|
|
|
|
|
let isRequestingMembershipProof = false;
|
|
|
|
|
|
|
|
const outerGroupCall = RingRTC.getGroupCall(groupIdBuffer, {
|
|
|
|
onLocalDeviceStateChanged: groupCall => {
|
|
|
|
const localDeviceState = groupCall.getLocalDeviceState();
|
|
|
|
|
|
|
|
if (localDeviceState.connectionState === ConnectionState.NotConnected) {
|
|
|
|
if (localDeviceState.videoMuted) {
|
|
|
|
this.disableLocalCamera();
|
|
|
|
}
|
|
|
|
|
|
|
|
delete this.callsByConversation[conversationId];
|
|
|
|
} else {
|
|
|
|
this.callsByConversation[conversationId] = groupCall;
|
|
|
|
|
|
|
|
if (localDeviceState.videoMuted) {
|
|
|
|
this.disableLocalCamera();
|
|
|
|
} else {
|
|
|
|
this.enableLocalCamera();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.syncGroupCallToRedux(conversationId, groupCall);
|
|
|
|
},
|
|
|
|
onRemoteDeviceStatesChanged: groupCall => {
|
|
|
|
this.syncGroupCallToRedux(conversationId, groupCall);
|
|
|
|
},
|
|
|
|
onJoinedMembersChanged: groupCall => {
|
|
|
|
this.syncGroupCallToRedux(conversationId, groupCall);
|
|
|
|
},
|
|
|
|
async requestMembershipProof(groupCall) {
|
|
|
|
if (isRequestingMembershipProof) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
isRequestingMembershipProof = true;
|
|
|
|
try {
|
|
|
|
const proof = await fetchMembershipProof({
|
|
|
|
publicParams,
|
|
|
|
secretParams,
|
|
|
|
});
|
|
|
|
if (proof) {
|
|
|
|
const proofArray = new TextEncoder().encode(proof);
|
|
|
|
groupCall.setMembershipProof(proofArray.buffer);
|
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
window.log.error('Failed to fetch membership proof', err);
|
|
|
|
} finally {
|
|
|
|
isRequestingMembershipProof = false;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
requestGroupMembers(groupCall) {
|
|
|
|
groupCall.setGroupMembers(
|
|
|
|
getMembershipList(conversationId).map(
|
|
|
|
member =>
|
|
|
|
new GroupMemberInfo(
|
|
|
|
uuidToArrayBuffer(member.uuid),
|
|
|
|
member.uuidCiphertext
|
|
|
|
)
|
|
|
|
)
|
|
|
|
);
|
|
|
|
},
|
|
|
|
onEnded: noop,
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!outerGroupCall) {
|
|
|
|
// This should be very rare, likely due to RingRTC not being able to get a lock
|
|
|
|
// or memory or something like that.
|
|
|
|
throw new Error('Failed to get a group call instance; cannot start call');
|
|
|
|
}
|
|
|
|
|
|
|
|
outerGroupCall.connect();
|
|
|
|
|
|
|
|
this.syncGroupCallToRedux(conversationId, outerGroupCall);
|
|
|
|
|
|
|
|
return outerGroupCall;
|
|
|
|
}
|
|
|
|
|
|
|
|
public joinGroupCall(
|
|
|
|
conversationId: string,
|
|
|
|
hasLocalAudio: boolean,
|
|
|
|
hasLocalVideo: boolean
|
|
|
|
): void {
|
|
|
|
const conversation = window.ConversationController.get(
|
|
|
|
conversationId
|
|
|
|
)?.format();
|
|
|
|
if (!conversation) {
|
|
|
|
window.log.error('Missing conversation; not joining group call');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (
|
|
|
|
!conversation.groupId ||
|
|
|
|
!conversation.publicParams ||
|
|
|
|
!conversation.secretParams
|
|
|
|
) {
|
|
|
|
window.log.error(
|
|
|
|
'Conversation is missing required parameters. Cannot join group call'
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const groupCall = this.connectGroupCall(conversationId, {
|
|
|
|
groupId: conversation.groupId,
|
|
|
|
publicParams: conversation.publicParams,
|
|
|
|
secretParams: conversation.secretParams,
|
|
|
|
});
|
|
|
|
|
|
|
|
groupCall.setOutgoingAudioMuted(!hasLocalAudio);
|
|
|
|
groupCall.setOutgoingVideoMuted(!hasLocalVideo);
|
|
|
|
this.videoCapturer.enableCaptureAndSend(groupCall);
|
|
|
|
|
|
|
|
groupCall.join();
|
|
|
|
}
|
|
|
|
|
2020-11-06 17:36:37 +00:00
|
|
|
private getCallIdForConversation(conversationId: string): undefined | CallId {
|
2020-11-13 19:57:55 +00:00
|
|
|
return this.getDirectCall(conversationId)?.callId;
|
|
|
|
}
|
|
|
|
|
|
|
|
// See the comment in types/Calling.ts to explain why we have to do this conversion.
|
|
|
|
private convertRingRtcConnectionState(
|
|
|
|
connectionState: ConnectionState
|
|
|
|
): GroupCallConnectionState {
|
|
|
|
switch (connectionState) {
|
|
|
|
case ConnectionState.NotConnected:
|
|
|
|
return GroupCallConnectionState.NotConnected;
|
|
|
|
case ConnectionState.Connecting:
|
|
|
|
return GroupCallConnectionState.Connecting;
|
|
|
|
case ConnectionState.Connected:
|
|
|
|
return GroupCallConnectionState.Connected;
|
|
|
|
case ConnectionState.Reconnecting:
|
|
|
|
return GroupCallConnectionState.Reconnecting;
|
|
|
|
default:
|
|
|
|
throw missingCaseError(connectionState);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// See the comment in types/Calling.ts to explain why we have to do this conversion.
|
|
|
|
private convertRingRtcJoinState(joinState: JoinState): GroupCallJoinState {
|
|
|
|
switch (joinState) {
|
|
|
|
case JoinState.NotJoined:
|
|
|
|
return GroupCallJoinState.NotJoined;
|
|
|
|
case JoinState.Joining:
|
|
|
|
return GroupCallJoinState.Joining;
|
|
|
|
case JoinState.Joined:
|
|
|
|
return GroupCallJoinState.Joined;
|
|
|
|
default:
|
|
|
|
throw missingCaseError(joinState);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private formatGroupCallForRedux(groupCall: GroupCall) {
|
|
|
|
const localDeviceState = groupCall.getLocalDeviceState();
|
|
|
|
|
|
|
|
// RingRTC doesn't ensure that the demux ID is unique. This can happen if someone
|
|
|
|
// leaves the call and quickly rejoins; RingRTC will tell us that there are two
|
|
|
|
// participants with the same demux ID in the call.
|
|
|
|
const remoteDeviceStates = uniqBy(
|
|
|
|
groupCall.getRemoteDeviceStates() || [],
|
|
|
|
remoteDeviceState => remoteDeviceState.demuxId
|
|
|
|
);
|
|
|
|
|
|
|
|
// It should be impossible to be disconnected and Joining or Joined. Just in case, we
|
|
|
|
// try to handle that case.
|
|
|
|
const joinState: GroupCallJoinState =
|
|
|
|
localDeviceState.connectionState === ConnectionState.NotConnected
|
|
|
|
? GroupCallJoinState.NotJoined
|
|
|
|
: this.convertRingRtcJoinState(localDeviceState.joinState);
|
|
|
|
|
|
|
|
return {
|
|
|
|
connectionState: this.convertRingRtcConnectionState(
|
|
|
|
localDeviceState.connectionState
|
|
|
|
),
|
|
|
|
joinState,
|
|
|
|
hasLocalAudio: !localDeviceState.audioMuted,
|
|
|
|
hasLocalVideo: !localDeviceState.videoMuted,
|
|
|
|
remoteParticipants: remoteDeviceStates.map(remoteDeviceState => ({
|
|
|
|
demuxId: remoteDeviceState.demuxId,
|
|
|
|
userId: arrayBufferToUuid(remoteDeviceState.userId) || '',
|
|
|
|
hasRemoteAudio: !remoteDeviceState.audioMuted,
|
|
|
|
hasRemoteVideo: !remoteDeviceState.videoMuted,
|
|
|
|
// If RingRTC doesn't send us an aspect ratio, we make a guess.
|
|
|
|
videoAspectRatio:
|
|
|
|
remoteDeviceState.videoAspectRatio ||
|
|
|
|
(remoteDeviceState.videoMuted ? 1 : 4 / 3),
|
|
|
|
})),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
public getGroupCallVideoFrameSource(
|
|
|
|
conversationId: string,
|
|
|
|
demuxId: number
|
|
|
|
): VideoFrameSource {
|
|
|
|
const groupCall = this.getGroupCall(conversationId);
|
|
|
|
if (!groupCall) {
|
|
|
|
throw new Error('Could not find matching call');
|
|
|
|
}
|
|
|
|
return groupCall.getVideoSource(demuxId);
|
|
|
|
}
|
|
|
|
|
|
|
|
private syncGroupCallToRedux(
|
|
|
|
conversationId: string,
|
|
|
|
groupCall: GroupCall
|
|
|
|
): void {
|
|
|
|
this.uxActions?.groupCallStateChange({
|
|
|
|
conversationId,
|
|
|
|
...this.formatGroupCallForRedux(groupCall),
|
|
|
|
});
|
2020-06-04 18:16:19 +00:00
|
|
|
}
|
|
|
|
|
2020-11-06 17:36:37 +00:00
|
|
|
async accept(conversationId: string, asVideoCall: boolean): Promise<void> {
|
2020-09-25 21:05:25 +00:00
|
|
|
window.log.info('CallingClass.accept()');
|
|
|
|
|
2020-11-06 17:36:37 +00:00
|
|
|
const callId = this.getCallIdForConversation(conversationId);
|
|
|
|
if (!callId) {
|
|
|
|
window.log.warn('Trying to accept a non-existent call');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-06-04 18:16:19 +00:00
|
|
|
const haveMediaPermissions = await this.requestPermissions(asVideoCall);
|
|
|
|
if (haveMediaPermissions) {
|
2020-08-27 00:03:42 +00:00
|
|
|
await this.startDeviceReselectionTimer();
|
|
|
|
RingRTC.setVideoCapturer(callId, this.videoCapturer);
|
|
|
|
RingRTC.setVideoRenderer(callId, this.videoRenderer);
|
2020-06-04 18:16:19 +00:00
|
|
|
RingRTC.accept(callId, asVideoCall);
|
|
|
|
} else {
|
|
|
|
window.log.info('Permissions were denied, call not allowed, hanging up.');
|
|
|
|
RingRTC.hangup(callId);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-06 17:36:37 +00:00
|
|
|
decline(conversationId: string): void {
|
2020-09-25 21:05:25 +00:00
|
|
|
window.log.info('CallingClass.decline()');
|
|
|
|
|
2020-11-06 17:36:37 +00:00
|
|
|
const callId = this.getCallIdForConversation(conversationId);
|
|
|
|
if (!callId) {
|
|
|
|
window.log.warn('Trying to decline a non-existent call');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-06-04 18:16:19 +00:00
|
|
|
RingRTC.decline(callId);
|
|
|
|
}
|
|
|
|
|
2020-11-06 17:36:37 +00:00
|
|
|
hangup(conversationId: string): void {
|
2020-09-25 21:05:25 +00:00
|
|
|
window.log.info('CallingClass.hangup()');
|
|
|
|
|
2020-11-13 19:57:55 +00:00
|
|
|
const call = getOwn(this.callsByConversation, conversationId);
|
|
|
|
if (!call) {
|
2020-11-06 17:36:37 +00:00
|
|
|
window.log.warn('Trying to hang up a non-existent call');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-11-13 19:57:55 +00:00
|
|
|
if (call instanceof Call) {
|
|
|
|
RingRTC.hangup(call.callId);
|
|
|
|
} else if (call instanceof GroupCall) {
|
|
|
|
// This ensures that we turn off our devices.
|
|
|
|
call.setOutgoingAudioMuted(true);
|
|
|
|
call.setOutgoingVideoMuted(true);
|
|
|
|
call.disconnect();
|
|
|
|
} else {
|
|
|
|
throw missingCaseError(call);
|
|
|
|
}
|
2020-06-04 18:16:19 +00:00
|
|
|
}
|
|
|
|
|
2020-11-06 17:36:37 +00:00
|
|
|
setOutgoingAudio(conversationId: string, enabled: boolean): void {
|
2020-11-13 19:57:55 +00:00
|
|
|
const call = getOwn(this.callsByConversation, conversationId);
|
|
|
|
if (!call) {
|
2020-11-06 17:36:37 +00:00
|
|
|
window.log.warn('Trying to set outgoing audio for a non-existent call');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-11-13 19:57:55 +00:00
|
|
|
if (call instanceof Call) {
|
|
|
|
RingRTC.setOutgoingAudio(call.callId, enabled);
|
|
|
|
} else if (call instanceof GroupCall) {
|
|
|
|
call.setOutgoingAudioMuted(!enabled);
|
|
|
|
} else {
|
|
|
|
throw missingCaseError(call);
|
|
|
|
}
|
2020-06-04 18:16:19 +00:00
|
|
|
}
|
|
|
|
|
2020-11-06 17:36:37 +00:00
|
|
|
setOutgoingVideo(conversationId: string, enabled: boolean): void {
|
2020-11-13 19:57:55 +00:00
|
|
|
const call = getOwn(this.callsByConversation, conversationId);
|
|
|
|
if (!call) {
|
2020-11-06 17:36:37 +00:00
|
|
|
window.log.warn('Trying to set outgoing video for a non-existent call');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-11-13 19:57:55 +00:00
|
|
|
if (call instanceof Call) {
|
|
|
|
RingRTC.setOutgoingVideo(call.callId, enabled);
|
|
|
|
} else if (call instanceof GroupCall) {
|
|
|
|
call.setOutgoingVideoMuted(!enabled);
|
|
|
|
} else {
|
|
|
|
throw missingCaseError(call);
|
|
|
|
}
|
2020-06-04 18:16:19 +00:00
|
|
|
}
|
|
|
|
|
2020-08-27 00:03:42 +00:00
|
|
|
private async startDeviceReselectionTimer(): Promise<void> {
|
|
|
|
// Poll once
|
|
|
|
await this.pollForMediaDevices();
|
|
|
|
// Start the timer
|
|
|
|
if (!this.deviceReselectionTimer) {
|
|
|
|
this.deviceReselectionTimer = setInterval(async () => {
|
|
|
|
await this.pollForMediaDevices();
|
|
|
|
}, 3000);
|
|
|
|
}
|
2020-06-04 18:16:19 +00:00
|
|
|
}
|
|
|
|
|
2020-08-27 00:03:42 +00:00
|
|
|
private stopDeviceReselectionTimer() {
|
|
|
|
if (this.deviceReselectionTimer) {
|
|
|
|
clearInterval(this.deviceReselectionTimer);
|
|
|
|
this.deviceReselectionTimer = undefined;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private mediaDeviceSettingsEqual(
|
|
|
|
a?: MediaDeviceSettings,
|
|
|
|
b?: MediaDeviceSettings
|
|
|
|
): boolean {
|
|
|
|
if (!a && !b) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
if (!a || !b) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (
|
|
|
|
a.availableCameras.length !== b.availableCameras.length ||
|
|
|
|
a.availableMicrophones.length !== b.availableMicrophones.length ||
|
|
|
|
a.availableSpeakers.length !== b.availableSpeakers.length
|
|
|
|
) {
|
|
|
|
return false;
|
|
|
|
}
|
2020-09-03 14:59:24 +00:00
|
|
|
for (let i = 0; i < a.availableCameras.length; i += 1) {
|
2020-08-27 00:03:42 +00:00
|
|
|
if (
|
|
|
|
a.availableCameras[i].deviceId !== b.availableCameras[i].deviceId ||
|
|
|
|
a.availableCameras[i].groupId !== b.availableCameras[i].groupId ||
|
|
|
|
a.availableCameras[i].label !== b.availableCameras[i].label
|
|
|
|
) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
2020-09-03 14:59:24 +00:00
|
|
|
for (let i = 0; i < a.availableMicrophones.length; i += 1) {
|
2020-08-27 00:03:42 +00:00
|
|
|
if (
|
|
|
|
a.availableMicrophones[i].name !== b.availableMicrophones[i].name ||
|
2020-08-27 19:21:10 +00:00
|
|
|
a.availableMicrophones[i].uniqueId !==
|
|
|
|
b.availableMicrophones[i].uniqueId
|
2020-08-27 00:03:42 +00:00
|
|
|
) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
2020-09-03 14:59:24 +00:00
|
|
|
for (let i = 0; i < a.availableSpeakers.length; i += 1) {
|
2020-08-27 00:03:42 +00:00
|
|
|
if (
|
|
|
|
a.availableSpeakers[i].name !== b.availableSpeakers[i].name ||
|
2020-08-27 19:21:10 +00:00
|
|
|
a.availableSpeakers[i].uniqueId !== b.availableSpeakers[i].uniqueId
|
2020-08-27 00:03:42 +00:00
|
|
|
) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (
|
|
|
|
(a.selectedCamera && !b.selectedCamera) ||
|
|
|
|
(!a.selectedCamera && b.selectedCamera) ||
|
|
|
|
(a.selectedMicrophone && !b.selectedMicrophone) ||
|
|
|
|
(!a.selectedMicrophone && b.selectedMicrophone) ||
|
|
|
|
(a.selectedSpeaker && !b.selectedSpeaker) ||
|
|
|
|
(!a.selectedSpeaker && b.selectedSpeaker)
|
|
|
|
) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (
|
|
|
|
a.selectedCamera &&
|
|
|
|
b.selectedCamera &&
|
|
|
|
a.selectedCamera !== b.selectedCamera
|
|
|
|
) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (
|
|
|
|
a.selectedMicrophone &&
|
|
|
|
b.selectedMicrophone &&
|
|
|
|
a.selectedMicrophone.index !== b.selectedMicrophone.index
|
|
|
|
) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (
|
|
|
|
a.selectedSpeaker &&
|
|
|
|
b.selectedSpeaker &&
|
|
|
|
a.selectedSpeaker.index !== b.selectedSpeaker.index
|
|
|
|
) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
private async pollForMediaDevices(): Promise<void> {
|
|
|
|
const newSettings = await this.getMediaDeviceSettings();
|
|
|
|
if (
|
|
|
|
!this.mediaDeviceSettingsEqual(this.lastMediaDeviceSettings, newSettings)
|
|
|
|
) {
|
|
|
|
window.log.info(
|
|
|
|
'MediaDevice: available devices changed (from->to)',
|
|
|
|
this.lastMediaDeviceSettings,
|
|
|
|
newSettings
|
|
|
|
);
|
2020-08-31 23:24:47 +00:00
|
|
|
|
|
|
|
await this.selectPreferredDevices(newSettings);
|
2020-08-27 00:03:42 +00:00
|
|
|
this.lastMediaDeviceSettings = newSettings;
|
|
|
|
this.uxActions?.refreshIODevices(newSettings);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async getMediaDeviceSettings(): Promise<MediaDeviceSettings> {
|
|
|
|
const availableMicrophones = RingRTC.getAudioInputs();
|
|
|
|
const preferredMicrophone = window.storage.get(
|
|
|
|
'preferred-audio-input-device'
|
|
|
|
);
|
|
|
|
const selectedMicIndex = this.findBestMatchingDeviceIndex(
|
|
|
|
availableMicrophones,
|
|
|
|
preferredMicrophone
|
|
|
|
);
|
|
|
|
const selectedMicrophone =
|
|
|
|
selectedMicIndex !== undefined
|
|
|
|
? availableMicrophones[selectedMicIndex]
|
|
|
|
: undefined;
|
|
|
|
|
|
|
|
const availableSpeakers = RingRTC.getAudioOutputs();
|
|
|
|
const preferredSpeaker = window.storage.get(
|
|
|
|
'preferred-audio-output-device'
|
|
|
|
);
|
|
|
|
const selectedSpeakerIndex = this.findBestMatchingDeviceIndex(
|
|
|
|
availableSpeakers,
|
|
|
|
preferredSpeaker
|
|
|
|
);
|
|
|
|
const selectedSpeaker =
|
|
|
|
selectedSpeakerIndex !== undefined
|
|
|
|
? availableSpeakers[selectedSpeakerIndex]
|
|
|
|
: undefined;
|
|
|
|
|
2020-09-04 18:27:12 +00:00
|
|
|
const availableCameras = await this.videoCapturer.enumerateDevices();
|
2020-08-27 00:03:42 +00:00
|
|
|
const preferredCamera = window.storage.get('preferred-video-input-device');
|
|
|
|
const selectedCamera = this.findBestMatchingCamera(
|
|
|
|
availableCameras,
|
|
|
|
preferredCamera
|
|
|
|
);
|
|
|
|
|
|
|
|
return {
|
|
|
|
availableMicrophones,
|
|
|
|
availableSpeakers,
|
|
|
|
selectedMicrophone,
|
|
|
|
selectedSpeaker,
|
|
|
|
availableCameras,
|
|
|
|
selectedCamera,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
findBestMatchingDeviceIndex(
|
|
|
|
available: Array<AudioDevice>,
|
|
|
|
preferred: AudioDevice | undefined
|
|
|
|
): number | undefined {
|
2020-10-22 21:40:31 +00:00
|
|
|
if (preferred) {
|
|
|
|
// Match by uniqueId first, if available
|
|
|
|
if (preferred.uniqueId) {
|
|
|
|
const matchIndex = available.findIndex(
|
|
|
|
d => d.uniqueId === preferred.uniqueId
|
|
|
|
);
|
|
|
|
if (matchIndex !== -1) {
|
|
|
|
return matchIndex;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Match by name second
|
|
|
|
const matchingNames = available.filter(d => d.name === preferred.name);
|
|
|
|
if (matchingNames.length > 0) {
|
|
|
|
return matchingNames[0].index;
|
2020-08-27 00:03:42 +00:00
|
|
|
}
|
|
|
|
}
|
2020-10-22 21:40:31 +00:00
|
|
|
// Nothing matches or no preference; take the first device if there are any
|
2020-08-27 00:03:42 +00:00
|
|
|
return available.length > 0 ? 0 : undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
findBestMatchingCamera(
|
|
|
|
available: Array<MediaDeviceInfo>,
|
|
|
|
preferred?: string
|
|
|
|
): string | undefined {
|
|
|
|
const matchingId = available.filter(d => d.deviceId === preferred);
|
|
|
|
const nonInfrared = available.filter(d => !d.label.includes('IR Camera'));
|
|
|
|
|
2020-09-03 14:59:24 +00:00
|
|
|
// By default, pick the first non-IR camera (but allow the user to pick the
|
|
|
|
// infrared if they so desire)
|
2020-08-27 00:03:42 +00:00
|
|
|
if (matchingId.length > 0) {
|
|
|
|
return matchingId[0].deviceId;
|
2020-09-09 00:46:29 +00:00
|
|
|
}
|
|
|
|
if (nonInfrared.length > 0) {
|
2020-08-27 00:03:42 +00:00
|
|
|
return nonInfrared[0].deviceId;
|
|
|
|
}
|
2020-09-09 00:46:29 +00:00
|
|
|
|
|
|
|
return undefined;
|
2020-08-27 00:03:42 +00:00
|
|
|
}
|
|
|
|
|
2020-09-03 14:59:24 +00:00
|
|
|
setPreferredMicrophone(device: AudioDevice): void {
|
2020-08-27 00:03:42 +00:00
|
|
|
window.log.info('MediaDevice: setPreferredMicrophone', device);
|
|
|
|
window.storage.put('preferred-audio-input-device', device);
|
|
|
|
RingRTC.setAudioInput(device.index);
|
|
|
|
}
|
|
|
|
|
2020-09-03 14:59:24 +00:00
|
|
|
setPreferredSpeaker(device: AudioDevice): void {
|
2020-08-27 00:03:42 +00:00
|
|
|
window.log.info('MediaDevice: setPreferredSpeaker', device);
|
|
|
|
window.storage.put('preferred-audio-output-device', device);
|
|
|
|
RingRTC.setAudioOutput(device.index);
|
|
|
|
}
|
|
|
|
|
2020-10-08 01:25:33 +00:00
|
|
|
enableLocalCamera(): void {
|
|
|
|
this.videoCapturer.enableCapture();
|
|
|
|
}
|
|
|
|
|
|
|
|
disableLocalCamera(): void {
|
|
|
|
this.videoCapturer.disable();
|
|
|
|
}
|
|
|
|
|
2020-09-03 14:59:24 +00:00
|
|
|
async setPreferredCamera(device: string): Promise<void> {
|
2020-08-27 00:03:42 +00:00
|
|
|
window.log.info('MediaDevice: setPreferredCamera', device);
|
|
|
|
window.storage.put('preferred-video-input-device', device);
|
|
|
|
await this.videoCapturer.setPreferredDevice(device);
|
2020-06-04 18:16:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async handleCallingMessage(
|
|
|
|
envelope: EnvelopeClass,
|
2020-09-28 19:02:35 +00:00
|
|
|
callingMessage: CallingMessage
|
2020-09-03 14:59:24 +00:00
|
|
|
): Promise<void> {
|
2020-09-25 21:05:25 +00:00
|
|
|
window.log.info('CallingClass.handleCallingMessage()');
|
|
|
|
|
2020-06-04 18:16:19 +00:00
|
|
|
const enableIncomingCalls = await window.getIncomingCallNotification();
|
|
|
|
if (callingMessage.offer && !enableIncomingCalls) {
|
|
|
|
// Drop offers silently if incoming call notifications are disabled.
|
|
|
|
window.log.info('Incoming calls are disabled, ignoring call offer.');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-09-12 00:53:19 +00:00
|
|
|
const remoteUserId = envelope.sourceUuid || envelope.source;
|
2020-06-04 18:16:19 +00:00
|
|
|
const remoteDeviceId = this.parseDeviceId(envelope.sourceDevice);
|
|
|
|
if (!remoteUserId || !remoteDeviceId || !this.localDeviceId) {
|
|
|
|
window.log.error('Missing identifier, ignoring call message.');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-09-12 00:53:19 +00:00
|
|
|
const senderIdentityRecord = window.textsecure.storage.protocol.getIdentityRecord(
|
|
|
|
remoteUserId
|
|
|
|
);
|
|
|
|
if (!senderIdentityRecord) {
|
|
|
|
window.log.error(
|
|
|
|
'Missing sender identity record; ignoring call message.'
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const senderIdentityKey = senderIdentityRecord.publicKey.slice(1); // Ignore the type header, it is not used.
|
|
|
|
|
|
|
|
const receiverIdentityRecord = window.textsecure.storage.protocol.getIdentityRecord(
|
|
|
|
window.textsecure.storage.user.getUuid() ||
|
|
|
|
window.textsecure.storage.user.getNumber()
|
|
|
|
);
|
|
|
|
if (!receiverIdentityRecord) {
|
|
|
|
window.log.error(
|
|
|
|
'Missing receiver identity record; ignoring call message.'
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const receiverIdentityKey = receiverIdentityRecord.publicKey.slice(1); // Ignore the type header, it is not used.
|
|
|
|
|
2020-09-28 19:02:35 +00:00
|
|
|
const conversation = window.ConversationController.get(remoteUserId);
|
|
|
|
if (!conversation) {
|
|
|
|
window.log.error('Missing conversation; ignoring call message.');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (callingMessage.offer && !conversation.getAccepted()) {
|
|
|
|
window.log.info(
|
|
|
|
'Conversation was not approved by user; rejecting call message.'
|
|
|
|
);
|
|
|
|
|
|
|
|
const hangup = new HangupMessage();
|
|
|
|
hangup.callId = callingMessage.offer.callId;
|
|
|
|
hangup.deviceId = remoteDeviceId;
|
|
|
|
hangup.type = HangupType.NeedPermission;
|
|
|
|
|
|
|
|
const message = new CallingMessage();
|
|
|
|
message.legacyHangup = hangup;
|
|
|
|
|
|
|
|
await this.handleOutgoingSignaling(remoteUserId, message);
|
|
|
|
|
|
|
|
this.addCallHistoryForFailedIncomingCall(
|
|
|
|
conversation,
|
|
|
|
callingMessage.offer.type === OfferType.VideoCall
|
|
|
|
);
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-11-13 19:57:55 +00:00
|
|
|
const sourceUuid = envelope.sourceUuid
|
|
|
|
? uuidToArrayBuffer(envelope.sourceUuid)
|
|
|
|
: null;
|
|
|
|
|
2020-06-25 21:42:32 +00:00
|
|
|
const messageAgeSec = envelope.messageAgeSec ? envelope.messageAgeSec : 0;
|
2020-06-04 18:16:19 +00:00
|
|
|
|
2020-09-25 21:05:25 +00:00
|
|
|
window.log.info('CallingClass.handleCallingMessage(): Handling in RingRTC');
|
|
|
|
|
2020-06-04 18:16:19 +00:00
|
|
|
RingRTC.handleCallingMessage(
|
|
|
|
remoteUserId,
|
2020-11-13 19:57:55 +00:00
|
|
|
sourceUuid,
|
2020-06-04 18:16:19 +00:00
|
|
|
remoteDeviceId,
|
|
|
|
this.localDeviceId,
|
|
|
|
messageAgeSec,
|
2020-09-12 00:53:19 +00:00
|
|
|
callingMessage,
|
|
|
|
senderIdentityKey,
|
|
|
|
receiverIdentityKey
|
2020-06-04 18:16:19 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-08-31 23:24:47 +00:00
|
|
|
private async selectPreferredDevices(
|
2020-08-27 00:03:42 +00:00
|
|
|
settings: MediaDeviceSettings
|
|
|
|
): Promise<void> {
|
2020-08-31 23:24:47 +00:00
|
|
|
if (
|
|
|
|
(!this.lastMediaDeviceSettings && settings.selectedCamera) ||
|
|
|
|
(this.lastMediaDeviceSettings &&
|
|
|
|
settings.selectedCamera &&
|
|
|
|
this.lastMediaDeviceSettings.selectedCamera !== settings.selectedCamera)
|
|
|
|
) {
|
|
|
|
window.log.info('MediaDevice: selecting camera', settings.selectedCamera);
|
|
|
|
await this.videoCapturer.setPreferredDevice(settings.selectedCamera);
|
|
|
|
}
|
|
|
|
|
2020-09-03 14:59:24 +00:00
|
|
|
// Assume that the MediaDeviceSettings have been obtained very recently and
|
|
|
|
// the index is still valid (no devices have been plugged in in between).
|
2020-08-27 00:03:42 +00:00
|
|
|
if (settings.selectedMicrophone) {
|
|
|
|
window.log.info(
|
|
|
|
'MediaDevice: selecting microphone',
|
|
|
|
settings.selectedMicrophone
|
|
|
|
);
|
|
|
|
RingRTC.setAudioInput(settings.selectedMicrophone.index);
|
|
|
|
}
|
2020-08-31 23:24:47 +00:00
|
|
|
|
2020-08-27 00:03:42 +00:00
|
|
|
if (settings.selectedSpeaker) {
|
|
|
|
window.log.info(
|
|
|
|
'MediaDevice: selecting speaker',
|
|
|
|
settings.selectedMicrophone
|
|
|
|
);
|
|
|
|
RingRTC.setAudioOutput(settings.selectedSpeaker.index);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-04 18:16:19 +00:00
|
|
|
private async requestCameraPermissions(): Promise<boolean> {
|
|
|
|
const cameraPermission = await window.getMediaCameraPermissions();
|
|
|
|
if (!cameraPermission) {
|
|
|
|
await window.showCallingPermissionsPopup(true);
|
|
|
|
|
|
|
|
// Check the setting again (from the source of truth).
|
|
|
|
return window.getMediaCameraPermissions();
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
private async requestMicrophonePermissions(): Promise<boolean> {
|
|
|
|
const microphonePermission = await window.getMediaPermissions();
|
|
|
|
if (!microphonePermission) {
|
|
|
|
await window.showCallingPermissionsPopup(false);
|
|
|
|
|
|
|
|
// Check the setting again (from the source of truth).
|
|
|
|
return window.getMediaPermissions();
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
private async requestPermissions(isVideoCall: boolean): Promise<boolean> {
|
|
|
|
const microphonePermission = await this.requestMicrophonePermissions();
|
|
|
|
if (microphonePermission) {
|
|
|
|
if (isVideoCall) {
|
|
|
|
return this.requestCameraPermissions();
|
|
|
|
}
|
2020-09-09 00:46:29 +00:00
|
|
|
|
|
|
|
return true;
|
2020-06-04 18:16:19 +00:00
|
|
|
}
|
2020-09-09 00:46:29 +00:00
|
|
|
|
|
|
|
return false;
|
2020-06-04 18:16:19 +00:00
|
|
|
}
|
|
|
|
|
2020-11-13 19:57:55 +00:00
|
|
|
private async handleSendCallMessage(
|
|
|
|
recipient: ArrayBuffer,
|
|
|
|
data: ArrayBuffer
|
|
|
|
): Promise<boolean> {
|
|
|
|
const userId = arrayBufferToUuid(recipient);
|
|
|
|
if (!userId) {
|
|
|
|
window.log.error('handleSendCallMessage(): bad recipient UUID');
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
const message = new CallingMessage();
|
|
|
|
message.opaque = new OpaqueMessage();
|
|
|
|
message.opaque.data = data;
|
|
|
|
return this.handleOutgoingSignaling(userId, message);
|
|
|
|
}
|
|
|
|
|
2020-06-04 18:16:19 +00:00
|
|
|
private async handleOutgoingSignaling(
|
|
|
|
remoteUserId: UserId,
|
2020-09-28 19:02:35 +00:00
|
|
|
message: CallingMessage
|
2020-06-25 21:41:33 +00:00
|
|
|
): Promise<boolean> {
|
2020-06-04 18:16:19 +00:00
|
|
|
const conversation = window.ConversationController.get(remoteUserId);
|
2020-07-10 18:28:49 +00:00
|
|
|
const sendOptions = conversation
|
|
|
|
? conversation.getSendOptions()
|
|
|
|
: undefined;
|
|
|
|
|
|
|
|
if (!window.textsecure.messaging) {
|
|
|
|
window.log.warn('handleOutgoingSignaling() returning false; offline');
|
|
|
|
return false;
|
|
|
|
}
|
2020-06-04 18:16:19 +00:00
|
|
|
|
|
|
|
try {
|
|
|
|
await window.textsecure.messaging.sendCallingMessage(
|
|
|
|
remoteUserId,
|
|
|
|
message,
|
|
|
|
sendOptions
|
|
|
|
);
|
|
|
|
|
|
|
|
window.log.info('handleOutgoingSignaling() completed successfully');
|
2020-06-25 21:41:33 +00:00
|
|
|
return true;
|
2020-06-04 18:16:19 +00:00
|
|
|
} catch (err) {
|
|
|
|
if (err && err.errors && err.errors.length > 0) {
|
|
|
|
window.log.error(
|
|
|
|
`handleOutgoingSignaling() failed: ${err.errors[0].reason}`
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
window.log.error('handleOutgoingSignaling() failed');
|
|
|
|
}
|
2020-06-25 21:41:33 +00:00
|
|
|
return false;
|
2020-06-04 18:16:19 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-29 23:20:05 +00:00
|
|
|
// If we return null here, we hang up the call.
|
2020-06-04 18:16:19 +00:00
|
|
|
private async handleIncomingCall(call: Call): Promise<CallSettings | null> {
|
2020-09-25 21:05:25 +00:00
|
|
|
window.log.info('CallingClass.handleIncomingCall()');
|
|
|
|
|
2020-06-04 18:16:19 +00:00
|
|
|
if (!this.uxActions || !this.localDeviceId) {
|
|
|
|
window.log.error('Missing required objects, ignoring incoming call.');
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
const conversation = window.ConversationController.get(call.remoteUserId);
|
|
|
|
if (!conversation) {
|
|
|
|
window.log.error('Missing conversation, ignoring incoming call.');
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
// The peer must be 'trusted' before accepting a call from them.
|
|
|
|
// This is mostly the safety number check, unverified meaning that they were
|
|
|
|
// verified before but now they are not.
|
|
|
|
const verifiedEnum = await conversation.safeGetVerified();
|
|
|
|
if (
|
|
|
|
verifiedEnum ===
|
|
|
|
window.textsecure.storage.protocol.VerifiedStatus.UNVERIFIED
|
|
|
|
) {
|
2020-07-23 18:59:44 +00:00
|
|
|
window.log.info(
|
|
|
|
`Peer is not trusted, ignoring incoming call for conversation: ${conversation.idForLogging()}`
|
|
|
|
);
|
2020-09-28 19:02:35 +00:00
|
|
|
this.addCallHistoryForFailedIncomingCall(
|
|
|
|
conversation,
|
|
|
|
call.isVideoCall
|
2020-07-23 18:59:44 +00:00
|
|
|
);
|
2020-06-04 18:16:19 +00:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.attachToCall(conversation, call);
|
|
|
|
|
2020-11-06 17:36:37 +00:00
|
|
|
this.uxActions.receiveIncomingCall({
|
|
|
|
conversationId: conversation.id,
|
|
|
|
isVideoCall: call.isVideoCall,
|
2020-06-04 18:16:19 +00:00
|
|
|
});
|
|
|
|
|
2020-09-25 21:05:25 +00:00
|
|
|
window.log.info('CallingClass.handleIncomingCall(): Proceeding');
|
|
|
|
|
2020-06-04 18:16:19 +00:00
|
|
|
return await this.getCallSettings(conversation);
|
|
|
|
} catch (err) {
|
|
|
|
window.log.error(`Ignoring incoming call: ${err.stack}`);
|
2020-09-28 19:02:35 +00:00
|
|
|
this.addCallHistoryForFailedIncomingCall(conversation, call.isVideoCall);
|
2020-06-04 18:16:19 +00:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private handleAutoEndedIncomingCallRequest(
|
|
|
|
remoteUserId: UserId,
|
|
|
|
reason: CallEndedReason
|
|
|
|
) {
|
|
|
|
const conversation = window.ConversationController.get(remoteUserId);
|
|
|
|
if (!conversation) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.addCallHistoryForAutoEndedIncomingCall(conversation, reason);
|
|
|
|
}
|
|
|
|
|
2020-09-24 20:57:54 +00:00
|
|
|
private attachToCall(conversation: ConversationModel, call: Call): void {
|
2020-11-06 17:36:37 +00:00
|
|
|
this.callsByConversation[conversation.id] = call;
|
|
|
|
|
2020-06-04 18:16:19 +00:00
|
|
|
const { uxActions } = this;
|
|
|
|
if (!uxActions) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let acceptedTime: number | undefined;
|
|
|
|
|
2020-09-03 14:59:24 +00:00
|
|
|
// eslint-disable-next-line no-param-reassign
|
2020-06-04 18:16:19 +00:00
|
|
|
call.handleStateChanged = () => {
|
|
|
|
if (call.state === CallState.Accepted) {
|
2020-11-06 17:36:37 +00:00
|
|
|
acceptedTime = acceptedTime || Date.now();
|
2020-06-04 18:16:19 +00:00
|
|
|
} else if (call.state === CallState.Ended) {
|
|
|
|
this.addCallHistoryForEndedCall(conversation, call, acceptedTime);
|
2020-08-27 00:03:42 +00:00
|
|
|
this.stopDeviceReselectionTimer();
|
|
|
|
this.lastMediaDeviceSettings = undefined;
|
2020-11-06 17:36:37 +00:00
|
|
|
delete this.callsByConversation[conversation.id];
|
2020-06-04 18:16:19 +00:00
|
|
|
}
|
|
|
|
uxActions.callStateChange({
|
2020-11-06 17:36:37 +00:00
|
|
|
conversationId: conversation.id,
|
|
|
|
acceptedTime,
|
2020-06-04 18:16:19 +00:00
|
|
|
callState: call.state,
|
2020-10-01 19:09:15 +00:00
|
|
|
callEndedReason: call.endedReason,
|
2020-11-06 17:36:37 +00:00
|
|
|
isIncoming: call.isIncoming,
|
|
|
|
isVideoCall: call.isVideoCall,
|
|
|
|
title: conversation.getTitle(),
|
2020-06-04 18:16:19 +00:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2020-09-03 14:59:24 +00:00
|
|
|
// eslint-disable-next-line no-param-reassign
|
2020-06-04 18:16:19 +00:00
|
|
|
call.handleRemoteVideoEnabled = () => {
|
|
|
|
uxActions.remoteVideoChange({
|
2020-11-06 17:36:37 +00:00
|
|
|
conversationId: conversation.id,
|
|
|
|
hasVideo: call.remoteVideoEnabled,
|
2020-06-04 18:16:19 +00:00
|
|
|
});
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
private async handleLogMessage(
|
|
|
|
level: CallLogLevel,
|
|
|
|
fileName: string,
|
|
|
|
line: number,
|
|
|
|
message: string
|
|
|
|
) {
|
|
|
|
switch (level) {
|
|
|
|
case CallLogLevel.Info:
|
|
|
|
window.log.info(`${fileName}:${line} ${message}`);
|
|
|
|
break;
|
|
|
|
case CallLogLevel.Warn:
|
|
|
|
window.log.warn(`${fileName}:${line} ${message}`);
|
|
|
|
break;
|
|
|
|
case CallLogLevel.Error:
|
|
|
|
window.log.error(`${fileName}:${line} ${message}`);
|
2020-09-03 14:59:24 +00:00
|
|
|
break;
|
|
|
|
default:
|
|
|
|
break;
|
2020-06-04 18:16:19 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-13 19:57:55 +00:00
|
|
|
private async handleSendHttpRequest(
|
|
|
|
requestId: number,
|
|
|
|
url: string,
|
|
|
|
method: HttpMethod,
|
|
|
|
headers: { [name: string]: string },
|
|
|
|
body: ArrayBuffer | undefined
|
|
|
|
) {
|
|
|
|
if (!window.textsecure.messaging) {
|
|
|
|
RingRTC.httpRequestFailed(requestId, 'We are offline');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const httpMethod = RINGRTC_HTTP_METHOD_TO_OUR_HTTP_METHOD.get(method);
|
|
|
|
if (httpMethod === undefined) {
|
|
|
|
RingRTC.httpRequestFailed(
|
|
|
|
requestId,
|
|
|
|
`Unknown method: ${JSON.stringify(method)}`
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let result;
|
|
|
|
try {
|
|
|
|
result = await window.textsecure.messaging.server.makeSfuRequest(
|
|
|
|
url,
|
|
|
|
httpMethod,
|
|
|
|
headers,
|
|
|
|
body
|
|
|
|
);
|
|
|
|
} catch (err) {
|
|
|
|
window.log.error('handleSendHttpRequest: fetch failed with error', err);
|
|
|
|
RingRTC.httpRequestFailed(requestId, String(err));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
RingRTC.receivedHttpResponse(
|
|
|
|
requestId,
|
|
|
|
result.response.status,
|
|
|
|
result.data
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-06-04 18:16:19 +00:00
|
|
|
private getRemoteUserIdFromConversation(
|
2020-09-24 20:57:54 +00:00
|
|
|
conversation: ConversationModel
|
|
|
|
): UserId | undefined | null {
|
2020-06-04 18:16:19 +00:00
|
|
|
const recipients = conversation.getRecipients();
|
|
|
|
if (recipients.length !== 1) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
return recipients[0];
|
|
|
|
}
|
|
|
|
|
|
|
|
private get localDeviceId(): DeviceId | null {
|
|
|
|
return this.parseDeviceId(window.textsecure.storage.user.getDeviceId());
|
|
|
|
}
|
|
|
|
|
|
|
|
private parseDeviceId(
|
|
|
|
deviceId: number | string | undefined
|
|
|
|
): DeviceId | null {
|
|
|
|
if (typeof deviceId === 'string') {
|
|
|
|
return parseInt(deviceId, 10);
|
|
|
|
}
|
|
|
|
if (typeof deviceId === 'number') {
|
|
|
|
return deviceId;
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
private async getCallSettings(
|
2020-09-24 20:57:54 +00:00
|
|
|
conversation: ConversationModel
|
2020-06-04 18:16:19 +00:00
|
|
|
): Promise<CallSettings> {
|
2020-07-10 18:28:49 +00:00
|
|
|
if (!window.textsecure.messaging) {
|
|
|
|
throw new Error('getCallSettings: offline!');
|
|
|
|
}
|
|
|
|
|
2020-06-04 18:16:19 +00:00
|
|
|
const iceServerJson = await window.textsecure.messaging.server.getIceServers();
|
|
|
|
|
|
|
|
const shouldRelayCalls = Boolean(await window.getAlwaysRelayCalls());
|
|
|
|
|
|
|
|
// If the peer is 'unknown', i.e. not in the contact list, force IP hiding.
|
2020-07-29 16:29:57 +00:00
|
|
|
const isContactUnknown = !conversation.isFromOrAddedByTrustedContact();
|
2020-06-04 18:16:19 +00:00
|
|
|
|
|
|
|
return {
|
|
|
|
iceServer: JSON.parse(iceServerJson),
|
|
|
|
hideIp: shouldRelayCalls || isContactUnknown,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
private addCallHistoryForEndedCall(
|
2020-09-24 20:57:54 +00:00
|
|
|
conversation: ConversationModel,
|
2020-06-04 18:16:19 +00:00
|
|
|
call: Call,
|
2020-09-03 14:59:24 +00:00
|
|
|
acceptedTimeParam: number | undefined
|
2020-06-04 18:16:19 +00:00
|
|
|
) {
|
2020-09-03 14:59:24 +00:00
|
|
|
let acceptedTime = acceptedTimeParam;
|
|
|
|
|
2020-06-04 18:16:19 +00:00
|
|
|
const { endedReason, isIncoming } = call;
|
|
|
|
const wasAccepted = Boolean(acceptedTime);
|
|
|
|
const isOutgoing = !isIncoming;
|
|
|
|
const wasDeclined =
|
|
|
|
!wasAccepted &&
|
|
|
|
(endedReason === CallEndedReason.Declined ||
|
|
|
|
endedReason === CallEndedReason.DeclinedOnAnotherDevice ||
|
|
|
|
(isIncoming && endedReason === CallEndedReason.LocalHangup) ||
|
2020-07-07 00:37:43 +00:00
|
|
|
(isOutgoing && endedReason === CallEndedReason.RemoteHangup) ||
|
|
|
|
(isOutgoing &&
|
|
|
|
endedReason === CallEndedReason.RemoteHangupNeedPermission));
|
2020-06-04 18:16:19 +00:00
|
|
|
if (call.endedReason === CallEndedReason.AcceptedOnAnotherDevice) {
|
|
|
|
acceptedTime = Date.now();
|
|
|
|
}
|
|
|
|
|
2020-10-08 01:26:42 +00:00
|
|
|
conversation.addCallHistory({
|
2020-06-04 18:16:19 +00:00
|
|
|
wasIncoming: call.isIncoming,
|
|
|
|
wasVideoCall: call.isVideoCall,
|
|
|
|
wasDeclined,
|
|
|
|
acceptedTime,
|
|
|
|
endedTime: Date.now(),
|
2020-10-08 01:26:42 +00:00
|
|
|
});
|
2020-06-04 18:16:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private addCallHistoryForFailedIncomingCall(
|
2020-09-24 20:57:54 +00:00
|
|
|
conversation: ConversationModel,
|
2020-09-28 19:02:35 +00:00
|
|
|
wasVideoCall: boolean
|
2020-06-04 18:16:19 +00:00
|
|
|
) {
|
2020-10-08 01:26:42 +00:00
|
|
|
conversation.addCallHistory({
|
2020-06-04 18:16:19 +00:00
|
|
|
wasIncoming: true,
|
2020-09-28 19:02:35 +00:00
|
|
|
wasVideoCall,
|
2020-06-04 18:16:19 +00:00
|
|
|
// Since the user didn't decline, make sure it shows up as a missed call instead
|
|
|
|
wasDeclined: false,
|
|
|
|
acceptedTime: undefined,
|
|
|
|
endedTime: Date.now(),
|
2020-10-08 01:26:42 +00:00
|
|
|
});
|
2020-06-04 18:16:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private addCallHistoryForAutoEndedIncomingCall(
|
2020-09-24 20:57:54 +00:00
|
|
|
conversation: ConversationModel,
|
2020-06-04 18:16:19 +00:00
|
|
|
_reason: CallEndedReason
|
|
|
|
) {
|
2020-10-08 01:26:42 +00:00
|
|
|
conversation.addCallHistory({
|
2020-06-04 18:16:19 +00:00
|
|
|
wasIncoming: true,
|
|
|
|
// We don't actually know, but it doesn't seem that important in this case,
|
|
|
|
// but we could maybe plumb this info through RingRTC
|
|
|
|
wasVideoCall: false,
|
|
|
|
// Since the user didn't decline, make sure it shows up as a missed call instead
|
|
|
|
wasDeclined: false,
|
|
|
|
acceptedTime: undefined,
|
|
|
|
endedTime: Date.now(),
|
2020-10-08 01:26:42 +00:00
|
|
|
});
|
2020-06-04 18:16:19 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export const calling = new CallingClass();
|