mirror of
https://github.com/oxen-io/session-desktop.git
synced 2023-12-14 02:12:57 +01:00
Merge branch 'webrtc-calls' of https://github.com/oxen-io/session-desktop into webrtc-calls
This commit is contained in:
commit
5cca398c8f
|
@ -53,6 +53,7 @@ window.lokiFeatureFlags = {
|
||||||
padOutgoingAttachments: true,
|
padOutgoingAttachments: true,
|
||||||
enablePinConversations: true,
|
enablePinConversations: true,
|
||||||
useUnsendRequests: false,
|
useUnsendRequests: false,
|
||||||
|
useCallMessage: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
window.isBeforeVersion = (toCheck, baseVersion) => {
|
window.isBeforeVersion = (toCheck, baseVersion) => {
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import _ from 'underscore';
|
import _ from 'underscore';
|
||||||
import { ConversationModel } from '../../../models/conversation';
|
|
||||||
// tslint:disable-next-line: no-submodule-imports
|
|
||||||
import { getConversationController } from '../../../session/conversations/ConversationController';
|
import { getConversationController } from '../../../session/conversations/ConversationController';
|
||||||
|
import { CallManager } from '../../../session/utils';
|
||||||
import { SessionButton, SessionButtonColor } from '../SessionButton';
|
import { SessionButton, SessionButtonColor } from '../SessionButton';
|
||||||
import { SessionWrapperModal } from '../SessionWrapperModal';
|
import { SessionWrapperModal } from '../SessionWrapperModal';
|
||||||
|
|
||||||
|
@ -20,24 +19,24 @@ export const CallWindow = styled.div`
|
||||||
|
|
||||||
// similar styling to modal header
|
// similar styling to modal header
|
||||||
const CallWindowHeader = styled.div`
|
const CallWindowHeader = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
padding: $session-margin-lg;
|
padding: $session-margin-lg;
|
||||||
|
|
||||||
font-family: $session-font-default;
|
font-family: $session-font-default;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
font-size: $session-font-md;
|
font-size: $session-font-md;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// TODO: Add proper styling for this
|
// TODO: Add proper styling for this
|
||||||
const StreamContainer = styled.div`
|
const VideoContainer = styled.video`
|
||||||
width: 200px;
|
width: 200px;
|
||||||
height: 200px;
|
height: 200px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const CallWindowInner = styled.div`
|
const CallWindowInner = styled.div`
|
||||||
|
@ -51,13 +50,13 @@ const CallWindowInner = styled.div`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const CallWindowControls = styled.div`
|
const CallWindowControls = styled.div`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 100%;
|
top: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
/* background: green; */
|
/* background: green; */
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
transform: translateY(-100%);
|
transform: translateY(-100%);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// type WindowPositionType = {
|
// type WindowPositionType = {
|
||||||
|
@ -65,111 +64,103 @@ const CallWindowControls = styled.div`
|
||||||
// left: string;
|
// left: string;
|
||||||
// } | null;
|
// } | null;
|
||||||
|
|
||||||
type CallStateType = 'connecting' | 'ongoing' | 'incoming' | null
|
type CallStateType = 'connecting' | 'ongoing' | 'incoming' | null;
|
||||||
|
|
||||||
|
const fakeCaller = '054774a456f15c7aca42fe8d245983549000311aaebcf58ce246250c41fe227676';
|
||||||
|
|
||||||
export const CallContainer = () => {
|
export const CallContainer = () => {
|
||||||
const conversations = getConversationController().getConversations();
|
const conversations = getConversationController().getConversations();
|
||||||
|
|
||||||
// TODO:
|
// TODO:
|
||||||
/**
|
/**
|
||||||
* Add mute input, deafen, end call, possibly add person to call
|
* Add mute input, deafen, end call, possibly add person to call
|
||||||
* duration - look at how duration calculated for recording.
|
* duration - look at how duration calculated for recording.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const [connectionState, setConnectionState] = useState<CallStateType>('incoming');
|
const [connectionState, setConnectionState] = useState<CallStateType>('incoming');
|
||||||
// const [callWindowPosition, setCallWindowPosition] = useState<WindowPositionType>(null)
|
// const [callWindowPosition, setCallWindowPosition] = useState<WindowPositionType>(null)
|
||||||
|
|
||||||
// picking a conversation at random to test with
|
// picking a conversation at random to test with
|
||||||
const randomConversation = _.sample(conversations) as ConversationModel;
|
const foundConvo = conversations.find(convo => convo.id === fakeCaller);
|
||||||
randomConversation.callState = 'incoming';
|
|
||||||
console.warn({ randConvo: randomConversation });
|
|
||||||
|
|
||||||
const firstCallingConvo = _.first(conversations.filter(convo => convo.callState !== undefined));
|
if (!foundConvo) {
|
||||||
|
throw new Error('fakeconvo not found');
|
||||||
|
}
|
||||||
|
foundConvo.callState = 'incoming';
|
||||||
|
console.warn('foundConvo: ', foundConvo);
|
||||||
|
|
||||||
//#region input handlers
|
const firstCallingConvo = _.first(conversations.filter(convo => convo.callState !== undefined));
|
||||||
const handleAcceptIncomingCall = () => {
|
|
||||||
console.warn('accept call');
|
|
||||||
|
|
||||||
if (firstCallingConvo) {
|
//#region input handlers
|
||||||
setConnectionState('connecting');
|
const handleAcceptIncomingCall = async () => {
|
||||||
firstCallingConvo.callState = 'connecting';
|
console.warn('accept call');
|
||||||
|
|
||||||
// some delay
|
if (firstCallingConvo) {
|
||||||
setConnectionState('ongoing');
|
setConnectionState('connecting');
|
||||||
firstCallingConvo.callState = 'ongoing';
|
firstCallingConvo.callState = 'connecting';
|
||||||
}
|
await CallManager.USER_acceptIncomingCallRequest(fakeCaller);
|
||||||
// set conversationState = setting up
|
// some delay
|
||||||
|
setConnectionState('ongoing');
|
||||||
|
firstCallingConvo.callState = 'ongoing';
|
||||||
}
|
}
|
||||||
|
// set conversationState = setting up
|
||||||
|
};
|
||||||
|
|
||||||
const handleDeclineIncomingCall = () => {
|
const handleDeclineIncomingCall = async () => {
|
||||||
// set conversation.callState = null or undefined
|
// set conversation.callState = null or undefined
|
||||||
// close the modal
|
// close the modal
|
||||||
if (firstCallingConvo) {
|
if (firstCallingConvo) {
|
||||||
firstCallingConvo.callState = undefined;
|
firstCallingConvo.callState = undefined;
|
||||||
}
|
|
||||||
console.warn('declined call');
|
|
||||||
}
|
}
|
||||||
|
console.warn('declined call');
|
||||||
|
await CallManager.USER_rejectIncomingCallRequest(fakeCaller);
|
||||||
|
};
|
||||||
|
|
||||||
const handleEndCall = () => {
|
const handleEndCall = async () => {
|
||||||
// call method to end call connection
|
// call method to end call connection
|
||||||
console.warn("ending the call");
|
console.warn('ending the call');
|
||||||
if (firstCallingConvo) {
|
await CallManager.USER_rejectIncomingCallRequest(fakeCaller);
|
||||||
firstCallingConvo.callState = undefined;
|
};
|
||||||
}
|
|
||||||
setConnectionState(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleMouseDown = () => {
|
const handleMouseDown = () => {
|
||||||
// reposition call window
|
// reposition call window
|
||||||
}
|
};
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
if (connectionState === null) {
|
return (
|
||||||
return null;
|
<>
|
||||||
}
|
{connectionState === 'connecting' ? 'connecting...' : null}
|
||||||
|
{connectionState === 'ongoing' ? (
|
||||||
|
<CallWindow onMouseDown={handleMouseDown}>
|
||||||
|
<CallWindowInner>
|
||||||
|
<CallWindowHeader>
|
||||||
|
{firstCallingConvo ? firstCallingConvo.getName() : 'Group name not found'}
|
||||||
|
</CallWindowHeader>
|
||||||
|
<VideoContainer />
|
||||||
|
<CallWindowControls>
|
||||||
|
<SessionButton text={'end call'} onClick={handleEndCall} />
|
||||||
|
<SessionButton text={'end call'} onClick={handleEndCall} />
|
||||||
|
</CallWindowControls>
|
||||||
|
</CallWindowInner>
|
||||||
|
</CallWindow>
|
||||||
|
) : null}
|
||||||
|
|
||||||
return (
|
{!connectionState ? (
|
||||||
<>
|
<SessionWrapperModal title={'incoming call'}>'none'</SessionWrapperModal>
|
||||||
{connectionState === 'connecting' ?
|
) : null}
|
||||||
'connecting...'
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
{connectionState === 'ongoing' ?
|
|
||||||
<CallWindow onMouseDown={handleMouseDown}>
|
|
||||||
<CallWindowInner>
|
|
||||||
<CallWindowHeader>
|
|
||||||
{ firstCallingConvo ? firstCallingConvo.getTitle() : 'Group name not found'}
|
|
||||||
</CallWindowHeader>
|
|
||||||
<StreamContainer></StreamContainer>
|
|
||||||
<CallWindowControls>
|
|
||||||
<SessionButton text={'end call'} onClick={handleEndCall} />
|
|
||||||
</CallWindowControls>
|
|
||||||
</CallWindowInner>
|
|
||||||
</CallWindow>
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
|
|
||||||
{!connectionState ?
|
{connectionState === 'incoming' ? (
|
||||||
<SessionWrapperModal title={'incoming call'}>
|
<SessionWrapperModal title={'incoming call'}>
|
||||||
'none'
|
<div className="session-modal__button-group">
|
||||||
</SessionWrapperModal>
|
<SessionButton text={'decline'} onClick={handleDeclineIncomingCall} />
|
||||||
: null
|
<SessionButton
|
||||||
}
|
text={'accept'}
|
||||||
|
onClick={handleAcceptIncomingCall}
|
||||||
{connectionState === 'incoming' ?
|
buttonColor={SessionButtonColor.Green}
|
||||||
<SessionWrapperModal title={`incoming call from ${firstCallingConvo?.getTitle()}`}>
|
/>
|
||||||
<div className="session-modal__button-group">
|
</div>
|
||||||
<SessionButton text={'decline'} onClick={handleDeclineIncomingCall} />
|
</SessionWrapperModal>
|
||||||
<SessionButton
|
) : null}
|
||||||
text={'accept'}
|
</>
|
||||||
onClick={handleAcceptIncomingCall}
|
);
|
||||||
buttonColor={SessionButtonColor.Green}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</SessionWrapperModal>
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
80
ts/receiver/callMessage.ts
Normal file
80
ts/receiver/callMessage.ts
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { SignalService } from '../protobuf';
|
||||||
|
import { TTL_DEFAULT } from '../session/constants';
|
||||||
|
import { SNodeAPI } from '../session/snode_api';
|
||||||
|
import { CallManager } from '../session/utils';
|
||||||
|
import { removeFromCache } from './cache';
|
||||||
|
import { EnvelopePlus } from './types';
|
||||||
|
|
||||||
|
// audric FIXME: refactor this out to persistence, just to help debug the flow and send/receive in synchronous testing
|
||||||
|
|
||||||
|
export async function handleCallMessage(
|
||||||
|
envelope: EnvelopePlus,
|
||||||
|
callMessage: SignalService.CallMessage
|
||||||
|
) {
|
||||||
|
const sender = envelope.senderIdentity || envelope.source;
|
||||||
|
|
||||||
|
const currentOffset = SNodeAPI.getLatestTimestampOffset();
|
||||||
|
const sentTimestamp = _.toNumber(envelope.timestamp);
|
||||||
|
|
||||||
|
const { type } = callMessage;
|
||||||
|
switch (type) {
|
||||||
|
case SignalService.CallMessage.Type.END_CALL:
|
||||||
|
window.log.info('handling callMessage END_CALL');
|
||||||
|
break;
|
||||||
|
case SignalService.CallMessage.Type.ANSWER:
|
||||||
|
window.log.info('handling callMessage ANSWER');
|
||||||
|
break;
|
||||||
|
case SignalService.CallMessage.Type.ICE_CANDIDATES:
|
||||||
|
window.log.info('handling callMessage ICE_CANDIDATES');
|
||||||
|
break;
|
||||||
|
case SignalService.CallMessage.Type.OFFER:
|
||||||
|
window.log.info('handling callMessage OFFER');
|
||||||
|
break;
|
||||||
|
case SignalService.CallMessage.Type.PROVISIONAL_ANSWER:
|
||||||
|
window.log.info('handling callMessage PROVISIONAL_ANSWER');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
window.log.info('handling callMessage unknown');
|
||||||
|
}
|
||||||
|
if (type === SignalService.CallMessage.Type.OFFER) {
|
||||||
|
if (Math.max(sentTimestamp - (Date.now() - currentOffset)) > TTL_DEFAULT.CALL_MESSAGE) {
|
||||||
|
window?.log?.info('Dropping incoming OFFER callMessage sent a while ago: ', sentTimestamp);
|
||||||
|
await removeFromCache(envelope);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await removeFromCache(envelope);
|
||||||
|
|
||||||
|
await CallManager.handleOfferCallMessage(sender, callMessage);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === SignalService.CallMessage.Type.END_CALL) {
|
||||||
|
await removeFromCache(envelope);
|
||||||
|
|
||||||
|
CallManager.handleEndCallMessage(sender);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === SignalService.CallMessage.Type.ANSWER) {
|
||||||
|
await removeFromCache(envelope);
|
||||||
|
|
||||||
|
await CallManager.handleCallAnsweredMessage(sender, callMessage);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (type === SignalService.CallMessage.Type.ICE_CANDIDATES) {
|
||||||
|
await removeFromCache(envelope);
|
||||||
|
|
||||||
|
await CallManager.handleIceCandidatesMessage(sender, callMessage);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await removeFromCache(envelope);
|
||||||
|
|
||||||
|
// if this another type of call message, just add it to the manager
|
||||||
|
await CallManager.handleOtherCallMessage(sender, callMessage);
|
||||||
|
}
|
|
@ -18,6 +18,7 @@ import { removeMessagePadding } from '../session/crypto/BufferPadding';
|
||||||
import { perfEnd, perfStart } from '../session/utils/Performance';
|
import { perfEnd, perfStart } from '../session/utils/Performance';
|
||||||
import { getAllCachedECKeyPair } from './closedGroups';
|
import { getAllCachedECKeyPair } from './closedGroups';
|
||||||
import { getMessageBySenderAndTimestamp } from '../data/data';
|
import { getMessageBySenderAndTimestamp } from '../data/data';
|
||||||
|
import { handleCallMessage } from './callMessage';
|
||||||
|
|
||||||
export async function handleContentMessage(envelope: EnvelopePlus, messageHash?: string) {
|
export async function handleContentMessage(envelope: EnvelopePlus, messageHash?: string) {
|
||||||
try {
|
try {
|
||||||
|
@ -399,6 +400,9 @@ export async function innerHandleContentMessage(
|
||||||
if (content.unsendMessage && window.lokiFeatureFlags?.useUnsendRequests) {
|
if (content.unsendMessage && window.lokiFeatureFlags?.useUnsendRequests) {
|
||||||
await handleUnsendMessage(envelope, content.unsendMessage as SignalService.Unsend);
|
await handleUnsendMessage(envelope, content.unsendMessage as SignalService.Unsend);
|
||||||
}
|
}
|
||||||
|
if (content.callMessage && window.lokiFeatureFlags?.useCallMessage) {
|
||||||
|
await handleCallMessage(envelope, content.callMessage as SignalService.CallMessage);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
window?.log?.warn(e);
|
window?.log?.warn(e);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,28 +3,30 @@ import { MessageParams } from '../Message';
|
||||||
import { ContentMessage } from '..';
|
import { ContentMessage } from '..';
|
||||||
import { signalservice } from '../../../../protobuf/compiled';
|
import { signalservice } from '../../../../protobuf/compiled';
|
||||||
import { TTL_DEFAULT } from '../../../constants';
|
import { TTL_DEFAULT } from '../../../constants';
|
||||||
interface CallMessageMessageParams extends MessageParams {
|
interface CallMessageParams extends MessageParams {
|
||||||
type: SignalService.CallMessage.Type;
|
type: SignalService.CallMessage.Type;
|
||||||
sdpMLineIndexes: Array<number>;
|
sdpMLineIndexes?: Array<number>;
|
||||||
sdpMids: Array<string>;
|
sdpMids?: Array<string>;
|
||||||
sdps: Array<string>;
|
sdps?: Array<string>;
|
||||||
referencedAttachmentTimestamp: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CallMessageMessage extends ContentMessage {
|
export class CallMessage extends ContentMessage {
|
||||||
public readonly type: signalservice.CallMessage.Type;
|
public readonly type: signalservice.CallMessage.Type;
|
||||||
public readonly sdpMLineIndexes: Array<number>;
|
public readonly sdpMLineIndexes?: Array<number>;
|
||||||
public readonly sdpMids: Array<string>;
|
public readonly sdpMids?: Array<string>;
|
||||||
public readonly sdps: Array<string>;
|
public readonly sdps?: Array<string>;
|
||||||
|
|
||||||
constructor(params: CallMessageMessageParams) {
|
constructor(params: CallMessageParams) {
|
||||||
super({ timestamp: params.timestamp, identifier: params.identifier });
|
super({ timestamp: params.timestamp, identifier: params.identifier });
|
||||||
this.type = params.type;
|
this.type = params.type;
|
||||||
this.sdpMLineIndexes = params.sdpMLineIndexes;
|
this.sdpMLineIndexes = params.sdpMLineIndexes;
|
||||||
this.sdpMids = params.sdpMids;
|
this.sdpMids = params.sdpMids;
|
||||||
this.sdps = params.sdps;
|
this.sdps = params.sdps;
|
||||||
// this does not make any sense
|
// this does not make any sense
|
||||||
if (this.type !== signalservice.CallMessage.Type.END_CALL && this.sdps.length === 0) {
|
if (
|
||||||
|
this.type !== signalservice.CallMessage.Type.END_CALL &&
|
||||||
|
(!this.sdps || this.sdps.length === 0)
|
||||||
|
) {
|
||||||
throw new Error('sdps must be set unless this is a END_CALL type message');
|
throw new Error('sdps must be set unless this is a END_CALL type message');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import { SyncMessageType } from '../utils/syncUtils';
|
||||||
import { OpenGroupRequestCommonType } from '../../opengroup/opengroupV2/ApiUtil';
|
import { OpenGroupRequestCommonType } from '../../opengroup/opengroupV2/ApiUtil';
|
||||||
import { OpenGroupVisibleMessage } from '../messages/outgoing/visibleMessage/OpenGroupVisibleMessage';
|
import { OpenGroupVisibleMessage } from '../messages/outgoing/visibleMessage/OpenGroupVisibleMessage';
|
||||||
import { UnsendMessage } from '../messages/outgoing/controlMessage/UnsendMessage';
|
import { UnsendMessage } from '../messages/outgoing/controlMessage/UnsendMessage';
|
||||||
|
import { CallMessage } from '../messages/outgoing/controlMessage/CallMessage';
|
||||||
|
|
||||||
type ClosedGroupMessageType =
|
type ClosedGroupMessageType =
|
||||||
| ClosedGroupVisibleMessage
|
| ClosedGroupVisibleMessage
|
||||||
|
@ -129,7 +130,7 @@ export class MessageQueue {
|
||||||
*/
|
*/
|
||||||
public async sendToPubKeyNonDurably(
|
public async sendToPubKeyNonDurably(
|
||||||
user: PubKey,
|
user: PubKey,
|
||||||
message: ClosedGroupNewMessage
|
message: ClosedGroupNewMessage | CallMessage
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
let rawMessage;
|
let rawMessage;
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -30,11 +30,11 @@ export const ERROR_CODE_NO_CONNECT = 'ENETUNREACH: No network connection.';
|
||||||
|
|
||||||
let latestTimestampOffset = Number.MAX_SAFE_INTEGER;
|
let latestTimestampOffset = Number.MAX_SAFE_INTEGER;
|
||||||
|
|
||||||
function handleTimestampOffset(request: string, snodeTimestamp: number) {
|
function handleTimestampOffset(_request: string, snodeTimestamp: number) {
|
||||||
if (snodeTimestamp && _.isNumber(snodeTimestamp) && snodeTimestamp > 1609419600 * 1000) {
|
if (snodeTimestamp && _.isNumber(snodeTimestamp) && snodeTimestamp > 1609419600 * 1000) {
|
||||||
// first january 2021. Arbitrary, just want to make sure the return timestamp is somehow valid and not some crazy low value
|
// first january 2021. Arbitrary, just want to make sure the return timestamp is somehow valid and not some crazy low value
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
window?.log?.info(`timestamp offset from request ${request}: ${now - snodeTimestamp}ms`);
|
// window?.log?.info(`timestamp offset from request ${request}: ${now - snodeTimestamp}ms`);
|
||||||
latestTimestampOffset = now - snodeTimestamp;
|
latestTimestampOffset = now - snodeTimestamp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
344
ts/session/utils/CallManager.ts
Normal file
344
ts/session/utils/CallManager.ts
Normal file
|
@ -0,0 +1,344 @@
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { SignalService } from '../../protobuf';
|
||||||
|
import { CallMessage } from '../messages/outgoing/controlMessage/CallMessage';
|
||||||
|
import { ed25519Str } from '../onions/onionPath';
|
||||||
|
import { getMessageQueue } from '../sending';
|
||||||
|
import { PubKey } from '../types';
|
||||||
|
|
||||||
|
const incomingCall = ({ sender }: { sender: string }) => {
|
||||||
|
return { type: 'incomingCall', payload: sender };
|
||||||
|
};
|
||||||
|
const endCall = ({ sender }: { sender: string }) => {
|
||||||
|
return { type: 'endCall', payload: sender };
|
||||||
|
};
|
||||||
|
const answerCall = ({ sender, sdps }: { sender: string; sdps: Array<string> }) => {
|
||||||
|
return {
|
||||||
|
type: 'answerCall',
|
||||||
|
payload: {
|
||||||
|
sender,
|
||||||
|
sdps,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This field stores all the details received by a sender about a call in separate messages.
|
||||||
|
*/
|
||||||
|
const callCache = new Map<string, Array<SignalService.CallMessage>>();
|
||||||
|
|
||||||
|
let peerConnection: RTCPeerConnection | null;
|
||||||
|
|
||||||
|
const ENABLE_VIDEO = true;
|
||||||
|
|
||||||
|
const configuration = {
|
||||||
|
configuration: {
|
||||||
|
offerToReceiveAudio: true,
|
||||||
|
offerToReceiveVideo: ENABLE_VIDEO,
|
||||||
|
},
|
||||||
|
iceServers: [
|
||||||
|
{ urls: 'stun:stun.l.google.com:19302' },
|
||||||
|
{ urls: 'stun:stun1.l.google.com:19302' },
|
||||||
|
{ urls: 'stun:stun2.l.google.com:19302' },
|
||||||
|
{ urls: 'stun:stun3.l.google.com:19302' },
|
||||||
|
{ urls: 'stun:stun4.l.google.com:19302' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// tslint:disable-next-line: function-name
|
||||||
|
export async function USER_callRecipient(recipient: string) {
|
||||||
|
window?.log?.info(`starting call with ${ed25519Str(recipient)}..`);
|
||||||
|
|
||||||
|
if (peerConnection) {
|
||||||
|
window.log.info('closing existing peerconnection');
|
||||||
|
peerConnection.close();
|
||||||
|
peerConnection = null;
|
||||||
|
}
|
||||||
|
peerConnection = new RTCPeerConnection(configuration);
|
||||||
|
|
||||||
|
const mediaDevices = await openMediaDevices();
|
||||||
|
mediaDevices.getTracks().map(track => {
|
||||||
|
window.log.info('USER_callRecipient adding track: ', track);
|
||||||
|
peerConnection?.addTrack(track, mediaDevices);
|
||||||
|
});
|
||||||
|
peerConnection.addEventListener('connectionstatechange', _event => {
|
||||||
|
window.log.info('peerConnection?.connectionState:', peerConnection?.connectionState);
|
||||||
|
if (peerConnection?.connectionState === 'connected') {
|
||||||
|
// Peers connected!
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
peerConnection.addEventListener('icecandidate', event => {
|
||||||
|
// window.log.warn('event.candidate', event.candidate);
|
||||||
|
|
||||||
|
if (event.candidate) {
|
||||||
|
iceCandidates.push(event.candidate);
|
||||||
|
void iceSenderDebouncer(recipient);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const offerDescription = await peerConnection.createOffer({
|
||||||
|
offerToReceiveAudio: true,
|
||||||
|
offerToReceiveVideo: ENABLE_VIDEO,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!offerDescription || !offerDescription.sdp || !offerDescription.sdp.length) {
|
||||||
|
window.log.warn(`failed to createOffer for recipient ${ed25519Str(recipient)}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await peerConnection.setLocalDescription(offerDescription);
|
||||||
|
const callOfferMessage = new CallMessage({
|
||||||
|
timestamp: Date.now(),
|
||||||
|
type: SignalService.CallMessage.Type.OFFER,
|
||||||
|
sdps: [offerDescription.sdp],
|
||||||
|
});
|
||||||
|
|
||||||
|
window.log.info('sending OFFER MESSAGE');
|
||||||
|
await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(recipient), callOfferMessage);
|
||||||
|
// FIXME audric dispatch UI update to show the calling UI
|
||||||
|
}
|
||||||
|
|
||||||
|
const iceCandidates: Array<RTCIceCandidate> = new Array();
|
||||||
|
const iceSenderDebouncer = _.debounce(async (recipient: string) => {
|
||||||
|
if (!iceCandidates) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const validCandidates = _.compact(
|
||||||
|
iceCandidates.map(c => {
|
||||||
|
if (
|
||||||
|
c.sdpMLineIndex !== null &&
|
||||||
|
c.sdpMLineIndex !== undefined &&
|
||||||
|
c.sdpMid !== null &&
|
||||||
|
c.candidate
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
sdpMLineIndex: c.sdpMLineIndex,
|
||||||
|
sdpMid: c.sdpMid,
|
||||||
|
candidate: c.candidate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const callIceCandicates = new CallMessage({
|
||||||
|
timestamp: Date.now(),
|
||||||
|
type: SignalService.CallMessage.Type.ICE_CANDIDATES,
|
||||||
|
sdpMLineIndexes: validCandidates.map(c => c.sdpMLineIndex),
|
||||||
|
sdpMids: validCandidates.map(c => c.sdpMid),
|
||||||
|
sdps: validCandidates.map(c => c.candidate),
|
||||||
|
});
|
||||||
|
window.log.info('sending ICE CANDIDATES MESSAGE to ', recipient);
|
||||||
|
|
||||||
|
await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(recipient), callIceCandicates);
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
const openMediaDevices = async () => {
|
||||||
|
return navigator.mediaDevices.getUserMedia({
|
||||||
|
video: ENABLE_VIDEO,
|
||||||
|
audio: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const findLastMessageTypeFromSender = (sender: string, msgType: SignalService.CallMessage.Type) => {
|
||||||
|
const msgCacheFromSender = callCache.get(sender);
|
||||||
|
if (!msgCacheFromSender) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const lastOfferMessage = _.findLast(msgCacheFromSender, m => m.type === msgType);
|
||||||
|
|
||||||
|
if (!lastOfferMessage) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return lastOfferMessage;
|
||||||
|
};
|
||||||
|
|
||||||
|
// tslint:disable-next-line: function-name
|
||||||
|
export async function USER_acceptIncomingCallRequest(fromSender: string) {
|
||||||
|
const msgCacheFromSender = callCache.get(fromSender);
|
||||||
|
if (!msgCacheFromSender) {
|
||||||
|
window?.log?.info(
|
||||||
|
'incoming call request cannot be accepted as the corresponding message is not found'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const lastOfferMessage = findLastMessageTypeFromSender(
|
||||||
|
fromSender,
|
||||||
|
SignalService.CallMessage.Type.OFFER
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!lastOfferMessage) {
|
||||||
|
window?.log?.info(
|
||||||
|
'incoming call request cannot be accepted as the corresponding message is not found'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (peerConnection) {
|
||||||
|
window.log.info('closing existing peerconnection');
|
||||||
|
peerConnection.close();
|
||||||
|
peerConnection = null;
|
||||||
|
}
|
||||||
|
peerConnection = new RTCPeerConnection(configuration);
|
||||||
|
const mediaDevices = await openMediaDevices();
|
||||||
|
mediaDevices.getTracks().map(track => {
|
||||||
|
window.log.info('USER_acceptIncomingCallRequest adding track ', track);
|
||||||
|
peerConnection?.addTrack(track, mediaDevices);
|
||||||
|
});
|
||||||
|
peerConnection.addEventListener('icecandidateerror', event => {
|
||||||
|
console.warn('icecandidateerror:', event);
|
||||||
|
});
|
||||||
|
|
||||||
|
peerConnection.addEventListener('negotiationneeded', async event => {
|
||||||
|
console.warn('negotiationneeded:', event);
|
||||||
|
});
|
||||||
|
peerConnection.addEventListener('signalingstatechange', event => {
|
||||||
|
console.warn('signalingstatechange:', event);
|
||||||
|
});
|
||||||
|
|
||||||
|
peerConnection.addEventListener('ontrack', event => {
|
||||||
|
console.warn('ontrack:', event);
|
||||||
|
});
|
||||||
|
peerConnection.addEventListener('connectionstatechange', _event => {
|
||||||
|
window.log.info('peerConnection?.connectionState:', peerConnection?.connectionState, _event);
|
||||||
|
if (peerConnection?.connectionState === 'connected') {
|
||||||
|
// Peers connected!
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { sdps } = lastOfferMessage;
|
||||||
|
if (!sdps || sdps.length === 0) {
|
||||||
|
window?.log?.info(
|
||||||
|
'incoming call request cannot be accepted as the corresponding sdps is empty'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await peerConnection.setRemoteDescription(
|
||||||
|
new RTCSessionDescription({ sdp: sdps[0], type: 'offer' })
|
||||||
|
);
|
||||||
|
|
||||||
|
const answer = await peerConnection.createAnswer({
|
||||||
|
offerToReceiveAudio: true,
|
||||||
|
offerToReceiveVideo: ENABLE_VIDEO,
|
||||||
|
});
|
||||||
|
if (!answer?.sdp || answer.sdp.length === 0) {
|
||||||
|
window.log.warn('failed to create answer');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await peerConnection.setLocalDescription(answer);
|
||||||
|
const answerSdp = answer.sdp;
|
||||||
|
const callAnswerMessage = new CallMessage({
|
||||||
|
timestamp: Date.now(),
|
||||||
|
type: SignalService.CallMessage.Type.ANSWER,
|
||||||
|
sdps: [answerSdp],
|
||||||
|
});
|
||||||
|
|
||||||
|
const lastCandidatesFromSender = findLastMessageTypeFromSender(
|
||||||
|
fromSender,
|
||||||
|
SignalService.CallMessage.Type.ICE_CANDIDATES
|
||||||
|
);
|
||||||
|
|
||||||
|
if (lastCandidatesFromSender) {
|
||||||
|
console.warn('found sender ice candicate message already sent. Using it');
|
||||||
|
for (let index = 0; index < lastCandidatesFromSender.sdps.length; index++) {
|
||||||
|
const sdp = lastCandidatesFromSender.sdps[index];
|
||||||
|
const sdpMLineIndex = lastCandidatesFromSender.sdpMLineIndexes[index];
|
||||||
|
const sdpMid = lastCandidatesFromSender.sdpMids[index];
|
||||||
|
const candicate = new RTCIceCandidate({ sdpMid, sdpMLineIndex, candidate: sdp });
|
||||||
|
await peerConnection.addIceCandidate(candicate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.log.info('sending ANSWER MESSAGE');
|
||||||
|
|
||||||
|
await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(fromSender), callAnswerMessage);
|
||||||
|
|
||||||
|
window.inboxStore?.dispatch(answerCall({ sender: fromSender, sdps }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// tslint:disable-next-line: function-name
|
||||||
|
export async function USER_rejectIncomingCallRequest(fromSender: string) {
|
||||||
|
const endCallMessage = new CallMessage({
|
||||||
|
type: SignalService.CallMessage.Type.END_CALL,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
callCache.delete(fromSender);
|
||||||
|
|
||||||
|
window.inboxStore?.dispatch(endCall({ sender: fromSender }));
|
||||||
|
window.log.info('sending END_CALL MESSAGE');
|
||||||
|
|
||||||
|
await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(fromSender), endCallMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleEndCallMessage(sender: string) {
|
||||||
|
callCache.delete(sender);
|
||||||
|
//
|
||||||
|
// FIXME audric trigger UI cleanup
|
||||||
|
window.inboxStore?.dispatch(endCall({ sender }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleOfferCallMessage(
|
||||||
|
sender: string,
|
||||||
|
callMessage: SignalService.CallMessage
|
||||||
|
) {
|
||||||
|
if (!callCache.has(sender)) {
|
||||||
|
callCache.set(sender, new Array());
|
||||||
|
}
|
||||||
|
callCache.get(sender)?.push(callMessage);
|
||||||
|
window.inboxStore?.dispatch(incomingCall({ sender }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleCallAnsweredMessage(
|
||||||
|
sender: string,
|
||||||
|
callMessage: SignalService.CallMessage
|
||||||
|
) {
|
||||||
|
if (!callMessage.sdps || callMessage.sdps.length === 0) {
|
||||||
|
window.log.warn('cannot handle answered message without sdps');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!callCache.has(sender)) {
|
||||||
|
callCache.set(sender, new Array());
|
||||||
|
}
|
||||||
|
|
||||||
|
callCache.get(sender)?.push(callMessage);
|
||||||
|
window.inboxStore?.dispatch(incomingCall({ sender }));
|
||||||
|
const remoteDesc = new RTCSessionDescription({ type: 'answer', sdp: callMessage.sdps[0] });
|
||||||
|
if (peerConnection) {
|
||||||
|
await peerConnection.setRemoteDescription(remoteDesc);
|
||||||
|
} else {
|
||||||
|
window.log.info('call answered by recipient but we do not have a peerconnection set');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleIceCandidatesMessage(
|
||||||
|
sender: string,
|
||||||
|
callMessage: SignalService.CallMessage
|
||||||
|
) {
|
||||||
|
if (!callMessage.sdps || callMessage.sdps.length === 0) {
|
||||||
|
window.log.warn('cannot handle iceCandicates message without candidates');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!callCache.has(sender)) {
|
||||||
|
callCache.set(sender, new Array());
|
||||||
|
}
|
||||||
|
|
||||||
|
callCache.get(sender)?.push(callMessage);
|
||||||
|
window.inboxStore?.dispatch(incomingCall({ sender }));
|
||||||
|
if (peerConnection) {
|
||||||
|
// tslint:disable-next-line: prefer-for-of
|
||||||
|
for (let index = 0; index < callMessage.sdps.length; index++) {
|
||||||
|
const sdp = callMessage.sdps[index];
|
||||||
|
const sdpMLineIndex = callMessage.sdpMLineIndexes[index];
|
||||||
|
const sdpMid = callMessage.sdpMids[index];
|
||||||
|
const candicate = new RTCIceCandidate({ sdpMid, sdpMLineIndex, candidate: sdp });
|
||||||
|
await peerConnection.addIceCandidate(candicate);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
window.log.info('handleIceCandidatesMessage but we do not have a peerconnection set');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// tslint:disable-next-line: no-async-without-await
|
||||||
|
export async function handleOtherCallMessage(
|
||||||
|
sender: string,
|
||||||
|
callMessage: SignalService.CallMessage
|
||||||
|
) {
|
||||||
|
callCache.get(sender)?.push(callMessage);
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ import * as UserUtils from './User';
|
||||||
import * as SyncUtils from './syncUtils';
|
import * as SyncUtils from './syncUtils';
|
||||||
import * as AttachmentsV2Utils from './AttachmentsV2';
|
import * as AttachmentsV2Utils from './AttachmentsV2';
|
||||||
import * as AttachmentDownloads from './AttachmentsDownload';
|
import * as AttachmentDownloads from './AttachmentsDownload';
|
||||||
|
import * as CallManager from './CallManager';
|
||||||
|
|
||||||
export * from './Attachments';
|
export * from './Attachments';
|
||||||
export * from './TypedEmitter';
|
export * from './TypedEmitter';
|
||||||
|
@ -26,4 +27,5 @@ export {
|
||||||
SyncUtils,
|
SyncUtils,
|
||||||
AttachmentsV2Utils,
|
AttachmentsV2Utils,
|
||||||
AttachmentDownloads,
|
AttachmentDownloads,
|
||||||
|
CallManager,
|
||||||
};
|
};
|
||||||
|
|
1
ts/window.d.ts
vendored
1
ts/window.d.ts
vendored
|
@ -48,6 +48,7 @@ declare global {
|
||||||
padOutgoingAttachments: boolean;
|
padOutgoingAttachments: boolean;
|
||||||
enablePinConversations: boolean;
|
enablePinConversations: boolean;
|
||||||
useUnsendRequests: boolean;
|
useUnsendRequests: boolean;
|
||||||
|
useCallMessage: boolean;
|
||||||
};
|
};
|
||||||
lokiSnodeAPI: LokiSnodeAPI;
|
lokiSnodeAPI: LokiSnodeAPI;
|
||||||
onLogin: any;
|
onLogin: any;
|
||||||
|
|
Loading…
Reference in a new issue