Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added perfect negotiation #1947

Merged
merged 3 commits into from
Oct 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion _locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -447,5 +447,11 @@
"incomingCall": "Incoming call",
"accept": "Accept",
"decline": "Decline",
"endCall": "End call"
"endCall": "End call",
"micAndCameraPermissionNeededTitle": "Camera and Microphone access required",
"micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy",
"unableToCall": "cancel your ongoing call first",
"unableToCallTitle": "Cannot start new call",
"callMissed": "Missed call from $name$",
"callMissedTitle": "Call missed"
}
1 change: 1 addition & 0 deletions ts/components/session/calling/CallContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ export const CallContainer = () => {
<CallWindowHeader>Call with: {ongoingCallProps.name}</CallWindowHeader>

<CallWindowInner>
<div>{hasIncomingCall}</div>
<VideoContainer>
<VideoContainerRemote ref={videoRefRemote} autoPlay={true} />
<VideoContainerLocal ref={videoRefLocal} autoPlay={true} />
Expand Down
29 changes: 14 additions & 15 deletions ts/components/session/menu/Menu.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import React from 'react';

import { getNumberOfPinnedConversations } from '../../../state/selectors/conversations';
import {
getHasIncomingCall,
getHasOngoingCall,
getNumberOfPinnedConversations,
} from '../../../state/selectors/conversations';
import { getFocusedSection } from '../../../state/selectors/section';
import { Item, Submenu } from 'react-contexify';
import {
Expand Down Expand Up @@ -319,32 +323,27 @@ export function getMarkAllReadMenuItem(conversationId: string): JSX.Element | nu
}

export function getStartCallMenuItem(conversationId: string): JSX.Element | null {
// TODO: possibly conditionally show options?
// const callOptions = [
// {
// name: 'Video call',
// value: 'video-call',
// },
// // {
// // name: 'Audio call',
// // value: 'audio-call',
// // },
// ];

const canCall = !(useSelector(getHasIncomingCall) || useSelector(getHasOngoingCall));
return (
<Item
onClick={async () => {
// TODO: either pass param to callRecipient or call different call methods based on item selected.
// TODO: one time redux-persisted permission modal?
const convo = getConversationController().get(conversationId);

if (!canCall) {
ToastUtils.pushUnableToCall();
return;
}

if (convo) {
convo.callState = 'connecting';
await convo.commit();

await CallManager.USER_callRecipient(convo.id);
}
}}
>
{'video call'}
{'Video Call'}
</Item>
);
}
Expand Down
2 changes: 1 addition & 1 deletion ts/receiver/callMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export async function handleCallMessage(
}
await removeFromCache(envelope);

CallManager.handleOfferCallMessage(sender, callMessage);
await CallManager.handleOfferCallMessage(sender, callMessage, sentTimestamp);

return;
}
Expand Down
2 changes: 1 addition & 1 deletion ts/receiver/closedGroups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -977,7 +977,7 @@ async function sendToGroupMembers(
window?.log?.info(`Creating a new group and an encryptionKeyPair for group ${groupPublicKey}`);
// evaluating if all invites sent, if failed give the option to retry failed invites via modal dialog
const inviteResults = await Promise.all(promises);
const allInvitesSent = _.every(inviteResults, Boolean);
const allInvitesSent = _.every(inviteResults, inviteResult => inviteResult !== false);

if (allInvitesSent) {
// if (true) {
Expand Down
4 changes: 2 additions & 2 deletions ts/session/sending/MessageQueue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export class MessageQueue {
public async sendToPubKeyNonDurably(
user: PubKey,
message: ClosedGroupNewMessage | CallMessage
): Promise<boolean> {
): Promise<boolean | number> {
let rawMessage;
try {
rawMessage = await MessageUtils.toRawMessage(user, message);
Expand All @@ -141,7 +141,7 @@ export class MessageQueue {
effectiveTimestamp,
wrappedEnvelope
);
return !!wrappedEnvelope;
return effectiveTimestamp;
} catch (error) {
if (rawMessage) {
await MessageSentHandler.handleMessageSentFailure(rawMessage, error);
Expand Down
127 changes: 71 additions & 56 deletions ts/session/utils/CallManager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import _ from 'lodash';
import { ToastUtils } from '.';
import { SessionSettingCategory } from '../../components/session/settings/SessionSettings';
import { getConversationById } from '../../data/data';
import { MessageModelType } from '../../models/messageType';
import { SignalService } from '../../protobuf';
import {
answerCall,
Expand All @@ -7,6 +11,8 @@ import {
incomingCall,
startingCallWith,
} from '../../state/ducks/conversations';
import { SectionType, showLeftPaneSection, showSettingsSection } from '../../state/ducks/section';
import { getConversationController } from '../conversations';
import { CallMessage } from '../messages/outgoing/controlMessage/CallMessage';
import { ed25519Str } from '../onions/onionPath';
import { getMessageQueue } from '../sending';
Expand All @@ -33,6 +39,7 @@ const ENABLE_VIDEO = true;
let makingOffer = false;
let ignoreOffer = false;
let isSettingRemoteAnswerPending = false;
let lastOutgoingOfferTimestamp = -Infinity;

const configuration = {
configuration: {
Expand Down Expand Up @@ -61,14 +68,16 @@ export async function USER_callRecipient(recipient: string) {

let mediaDevices: any;
try {
const mediaDevices = await openMediaDevices();
mediaDevices.getTracks().map(track => {
mediaDevices = await openMediaDevices();
mediaDevices.getTracks().map((track: any) => {
window.log.info('USER_callRecipient adding track: ', track);
peerConnection?.addTrack(track, mediaDevices);
});
} catch (err) {
console.error('Failed to open media devices. Check camera and mic app permissions');
// TODO: implement toast popup
ToastUtils.pushMicAndCameraPermissionNeeded(() => {
window.inboxStore?.dispatch(showLeftPaneSection(SectionType.Settings));
window.inboxStore?.dispatch(showSettingsSection(SessionSettingCategory.Privacy));
});
}
peerConnection.addEventListener('connectionstatechange', _event => {
window.log.info('peerConnection?.connectionState caller :', peerConnection?.connectionState);
Expand All @@ -77,7 +86,7 @@ export async function USER_callRecipient(recipient: string) {
}
});
peerConnection.addEventListener('ontrack', event => {
console.warn('ontrack:', event);
window.log?.warn('ontrack:', event);
});
peerConnection.addEventListener('icecandidate', event => {
// window.log.warn('event.candidate', event.candidate);
Expand All @@ -89,42 +98,33 @@ export async function USER_callRecipient(recipient: string) {
});
// peerConnection.addEventListener('negotiationneeded', async event => {
peerConnection.onnegotiationneeded = async event => {
console.warn('negotiationneeded:', event);
window.log?.warn('negotiationneeded:', event);
try {
makingOffer = true;
// const offerDescription = await peerConnection?.createOffer({
// offerToReceiveAudio: true,
// offerToReceiveVideo: true,
// });
// if (!offerDescription) {
// console.error('Failed to create offer for negotiation');
// return;
// }
// await peerConnection?.setLocalDescription(offerDescription);
// if (!offerDescription || !offerDescription.sdp || !offerDescription.sdp.length) {
// // window.log.warn(`failed to createOffer for recipient ${ed25519Str(recipient)}`);
// console.warn(`failed to createOffer for recipient ${ed25519Str(recipient)}`);
// return;
// }

// @ts-ignore
await peerConnection?.setLocalDescription();
let offer = await peerConnection?.createOffer();
console.warn(offer);
const offer = await peerConnection?.createOffer();
window.log?.warn(offer);

if (offer && offer.sdp) {
const callOfferMessage = new CallMessage({
const negotationOfferMessage = new CallMessage({
timestamp: Date.now(),
type: SignalService.CallMessage.Type.OFFER,
// sdps: [offerDescription.sdp],
sdps: [offer.sdp],
});

window.log.info('sending OFFER MESSAGE');
await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(recipient), callOfferMessage);
const negotationOfferSendResult = await getMessageQueue().sendToPubKeyNonDurably(
PubKey.cast(recipient),
negotationOfferMessage
);
if (typeof negotationOfferSendResult === 'number') {
window.log?.warn('setting last sent timestamp');
lastOutgoingOfferTimestamp = negotationOfferSendResult;
}
// debug: await new Promise(r => setTimeout(r, 10000)); adding artificial wait for offer debugging
}
} catch (err) {
console.error(err);
window.log?.error(`Error on handling negotiation needed ${err}`);
} finally {
makingOffer = false;
Expand Down Expand Up @@ -154,14 +154,21 @@ export async function USER_callRecipient(recipient: string) {
return;
}
await peerConnection.setLocalDescription(offerDescription);
const callOfferMessage = new CallMessage({
const offerMessage = 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);
const offerSendResult = await getMessageQueue().sendToPubKeyNonDurably(
PubKey.cast(recipient),
offerMessage
);
if (typeof offerSendResult === 'number') {
window.log?.warn('setting timestamp');
lastOutgoingOfferTimestamp = offerSendResult;
}
// FIXME audric dispatch UI update to show the calling UI
}

Expand Down Expand Up @@ -255,13 +262,13 @@ export async function USER_acceptIncomingCallRequest(fromSender: string) {
const remoteStream = new MediaStream();

peerConnection.addEventListener('icecandidate', event => {
console.warn('icecandidateerror:', event);
window.log?.warn('icecandidateerror:', event);
// TODO: ICE stuff
// signaler.send({candidate}); // probably event.candidate
});

peerConnection.addEventListener('signalingstatechange', event => {
console.warn('signalingstatechange:', event);
window.log?.warn('signalingstatechange:', event);
});

if (videoEventsListener) {
Expand Down Expand Up @@ -363,37 +370,26 @@ export function handleEndCallMessage(sender: string) {

export async function handleOfferCallMessage(
sender: string,
callMessage: SignalService.CallMessage
callMessage: SignalService.CallMessage,
incomingOfferTimestamp: number
) {
try {
console.warn({ callMessage });
const convos = getConversationController().getConversations();
if (convos.some(convo => convo.callState !== undefined)) {
await handleMissedCall(sender, incomingOfferTimestamp);
return;
}

const readyForOffer =
!makingOffer && (peerConnection?.signalingState == 'stable' || isSettingRemoteAnswerPending);
// TODO: How should politeness be decided between client / recipient?
ignoreOffer = !true && !readyForOffer;
!makingOffer && (peerConnection?.signalingState === 'stable' || isSettingRemoteAnswerPending);
const polite = lastOutgoingOfferTimestamp < incomingOfferTimestamp;
ignoreOffer = !polite && !readyForOffer;
if (ignoreOffer) {
// window.log?.warn('Received offer when unready for offer; Ignoring offer.');
console.warn('Received offer when unready for offer; Ignoring offer.');
window.log?.warn('Received offer when unready for offer; Ignoring offer.');
return;
}

// const description = await peerConnection?.createOffer({
// const description = await peerConnection?.createOffer({
// offerToReceiveVideo: true,
// offerToReceiveAudio: true,
// })

// @ts-ignore
await peerConnection?.setLocalDescription();
console.warn(peerConnection?.localDescription);

const message = new CallMessage({
type: SignalService.CallMessage.Type.ANSWER,
timestamp: Date.now(),
});

await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(sender), message);
// TODO: send via our signalling with the sdp of our pc.localDescription
// don't need to do the sending here as we dispatch an answer in a
} catch (err) {
window.log?.error(`Error handling offer message ${err}`);
}
Expand All @@ -405,6 +401,24 @@ export async function handleOfferCallMessage(
window.inboxStore?.dispatch(incomingCall({ pubkey: sender }));
}

async function handleMissedCall(sender: string, incomingOfferTimestamp: number) {
const incomingCallConversation = await getConversationById(sender);
ToastUtils.pushedMissedCall(incomingCallConversation?.getNickname() || 'Unknown');

await incomingCallConversation?.addSingleMessage({
conversationId: incomingCallConversation.id,
source: sender,
type: 'incoming' as MessageModelType,
sent_at: incomingOfferTimestamp,
received_at: Date.now(),
expireTimer: 0,
body: 'Missed call',
unread: 1,
});
incomingCallConversation?.updateLastMessage();
return;
}

export async function handleCallAnsweredMessage(
sender: string,
callMessage: SignalService.CallMessage
Expand All @@ -421,7 +435,7 @@ export async function handleCallAnsweredMessage(
window.inboxStore?.dispatch(answerCall({ pubkey: sender }));
const remoteDesc = new RTCSessionDescription({ type: 'answer', sdp: callMessage.sdps[0] });
if (peerConnection) {
console.warn('Setting remote answer pending');
window.log?.warn('Setting remote answer pending');
isSettingRemoteAnswerPending = true;
await peerConnection.setRemoteDescription(remoteDesc);
isSettingRemoteAnswerPending = false;
Expand Down Expand Up @@ -455,6 +469,7 @@ export async function handleIceCandidatesMessage(
await peerConnection.addIceCandidate(candicate);
} catch (err) {
if (!ignoreOffer) {
window.log?.warn('Error handling ICE candidates message');
}
}
}
Expand Down
21 changes: 21 additions & 0 deletions ts/session/utils/Toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,27 @@ export function pushMessageDeleteForbidden() {
pushToastError('messageDeletionForbidden', window.i18n('messageDeletionForbidden'));
}

export function pushUnableToCall() {
pushToastError('unableToCall', window.i18n('unableToCallTitle'), window.i18n('unableToCall'));
}

export function pushedMissedCall(conversationName: string) {
pushToastInfo(
'missedCall',
window.i18n('callMissedTitle'),
window.i18n('callMissedTitle', conversationName)
);
}

export function pushMicAndCameraPermissionNeeded(onClicked: () => void) {
pushToastInfo(
'micAndCameraPermissionNeeded',
window.i18n('micAndCameraPermissionNeededTitle'),
window.i18n('micAndCameraPermissionNeeded'),
onClicked
);
}

export function pushAudioPermissionNeeded(onClicked: () => void) {
pushToastInfo(
'audioPermissionNeeded',
Expand Down