create offer and answer ourselves and do not use the negotiation needed

event.

this event is causing us to loop in negotiation needed when each side
try to create one, gets the answer and so on...
pull/2039/head
Audric Ackermann 4 years ago
parent 1dff310820
commit d5f6180ae6
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4

@ -25,7 +25,10 @@ import { PnServer } from '../../pushnotification';
import { setIsRinging } from './RingingManager'; import { setIsRinging } from './RingingManager';
export type InputItem = { deviceId: string; label: string }; export type InputItem = { deviceId: string; label: string };
// tslint:disable: function-name
const maxWidth = 1920;
const maxHeight = 1080;
/** /**
* This uuid is set only once we accepted a call or started one. * This uuid is set only once we accepted a call or started one.
*/ */
@ -166,6 +169,28 @@ if (typeof navigator !== 'undefined') {
}); });
} }
const silence = () => {
const ctx = new AudioContext();
const oscillator = ctx.createOscillator();
const dst = oscillator.connect(ctx.createMediaStreamDestination());
oscillator.start();
return Object.assign((dst as any).stream.getAudioTracks()[0], { enabled: false });
};
const black = () => {
const canvas = Object.assign(document.createElement('canvas'), {
width: maxWidth,
height: maxHeight,
});
canvas.getContext('2d')?.fillRect(0, 0, maxWidth, maxHeight);
const stream = (canvas as any).captureStream();
return Object.assign(stream.getVideoTracks()[0], { enabled: false });
};
const getBlackSilenceMediaStream = () => {
return new MediaStream([black(), silence()]);
};
async function updateConnectedDevices() { async function updateConnectedDevices() {
// Get the set of cameras connected // Get the set of cameras connected
const videoCameras = await getConnectedDevices('videoinput'); const videoCameras = await getConnectedDevices('videoinput');
@ -219,6 +244,14 @@ export async function selectCameraByDeviceId(cameraDeviceId: string) {
if (sender?.track) { if (sender?.track) {
sender.track.enabled = false; sender.track.enabled = false;
} }
// do the same changes locally
localStream?.getVideoTracks().forEach(t => {
t.stop();
localStream?.removeTrack(t);
});
localStream?.addTrack(getBlackSilenceMediaStream().getVideoTracks()[0]);
sendVideoStatusViaDataChannel(); sendVideoStatusViaDataChannel();
callVideoListeners(); callVideoListeners();
return; return;
@ -235,24 +268,23 @@ export async function selectCameraByDeviceId(cameraDeviceId: string) {
try { try {
const newVideoStream = await navigator.mediaDevices.getUserMedia(devicesConfig); const newVideoStream = await navigator.mediaDevices.getUserMedia(devicesConfig);
const videoTrack = newVideoStream.getVideoTracks()[0]; const videoTrack = newVideoStream.getVideoTracks()[0];
if (!peerConnection) { if (!peerConnection) {
throw new Error('cannot selectCameraByDeviceId without a peer connection'); throw new Error('cannot selectCameraByDeviceId without a peer connection');
} }
// video might be completely off on start. adding a track like this triggers a negotationneeded event window.log.info('replacing video track');
window.log.info('adding/replacing video track'); const videoSender = peerConnection
const sender = peerConnection.getSenders().find(s => { .getTransceivers()
return s.track?.kind === videoTrack.kind; .find(t => t.sender.track?.kind === 'video')?.sender;
});
videoTrack.enabled = true; videoTrack.enabled = true;
if (sender) { if (videoSender) {
// this should not trigger a negotationneeded event await videoSender.replaceTrack(videoTrack);
// and it is needed for when the video cam was never turn on
await sender.replaceTrack(videoTrack);
} else { } else {
// this will trigger a negotiationeeded event throw new Error(
peerConnection.addTrack(videoTrack, newVideoStream); 'We should always have a videoSender as we are using a black video when no camera are in use'
);
} }
// do the same changes locally // do the same changes locally
@ -266,6 +298,7 @@ export async function selectCameraByDeviceId(cameraDeviceId: string) {
callVideoListeners(); callVideoListeners();
} catch (e) { } catch (e) {
window.log.warn('selectCameraByDeviceId failed with', e.message); window.log.warn('selectCameraByDeviceId failed with', e.message);
ToastUtils.pushToastError('selectCamera', e.message);
callVideoListeners(); callVideoListeners();
} }
} }
@ -281,6 +314,12 @@ export async function selectAudioInputByDeviceId(audioInputDeviceId: string) {
if (sender?.track) { if (sender?.track) {
sender.track.enabled = false; sender.track.enabled = false;
} }
// do the same changes locally
localStream?.getAudioTracks().forEach(t => {
t.stop();
localStream?.removeTrack(t);
});
localStream?.addTrack(getBlackSilenceMediaStream().getAudioTracks()[0]);
callVideoListeners(); callVideoListeners();
return; return;
} }
@ -295,6 +334,7 @@ export async function selectAudioInputByDeviceId(audioInputDeviceId: string) {
try { try {
const newAudioStream = await navigator.mediaDevices.getUserMedia(devicesConfig); const newAudioStream = await navigator.mediaDevices.getUserMedia(devicesConfig);
const audioTrack = newAudioStream.getAudioTracks()[0]; const audioTrack = newAudioStream.getAudioTracks()[0];
if (!peerConnection) { if (!peerConnection) {
throw new Error('cannot selectAudioInputByDeviceId without a peer connection'); throw new Error('cannot selectAudioInputByDeviceId without a peer connection');
@ -331,18 +371,15 @@ export async function selectAudioOutputByDeviceId(audioOutputDeviceId: string) {
} }
} }
async function handleNegotiationNeededEvent(recipient: string) { async function createOfferAndSendIt(recipient: string) {
try { try {
makingOffer = true; makingOffer = true;
window.log.info('got handleNegotiationNeeded event. creating offer'); window.log.info('got createOfferAndSendIt event. creating offer');
const offer = await peerConnection?.createOffer({ await (peerConnection as any)?.setLocalDescription();
offerToReceiveAudio: true, const offer = peerConnection?.localDescription;
offerToReceiveVideo: true,
});
if (!offer) { if (!offer) {
throw new Error('Could not create an offer'); throw new Error('Could not create an offer');
} }
await peerConnection?.setLocalDescription(offer);
if (!currentCallUUID) { if (!currentCallUUID) {
window.log.warn('cannot send offer without a currentCallUUID'); window.log.warn('cannot send offer without a currentCallUUID');
@ -357,18 +394,18 @@ async function handleNegotiationNeededEvent(recipient: string) {
uuid: currentCallUUID, uuid: currentCallUUID,
}); });
window.log.info(`sending OFFER MESSAGE with callUUID: ${currentCallUUID}`); window.log.info(`sending '${offer.type}'' with callUUID: ${currentCallUUID}`);
const negotationOfferSendResult = await getMessageQueue().sendToPubKeyNonDurably( const negotiationOfferSendResult = await getMessageQueue().sendToPubKeyNonDurably(
PubKey.cast(recipient), PubKey.cast(recipient),
offerMessage offerMessage
); );
if (typeof negotationOfferSendResult === 'number') { if (typeof negotiationOfferSendResult === 'number') {
// window.log?.warn('setting last sent timestamp'); // window.log?.warn('setting last sent timestamp');
lastOutgoingOfferTimestamp = negotationOfferSendResult; lastOutgoingOfferTimestamp = negotiationOfferSendResult;
} }
} }
} catch (err) { } catch (err) {
window.log?.error(`Error on handling negotiation needed ${err}`); window.log?.error(`Error createOfferAndSendIt ${err}`);
} finally { } finally {
makingOffer = false; makingOffer = false;
} }
@ -390,23 +427,13 @@ async function openMediaDevicesAndAddTracks() {
return; return;
} }
selectedAudioInputId = audioInputsList[0].deviceId; selectedAudioInputId = DEVICE_DISABLED_DEVICE_ID; //audioInputsList[0].deviceId;
selectedCameraId = DEVICE_DISABLED_DEVICE_ID; selectedCameraId = DEVICE_DISABLED_DEVICE_ID;
window.log.info( window.log.info(
`openMediaDevices videoDevice:${selectedCameraId} audioDevice:${selectedAudioInputId}` `openMediaDevices videoDevice:${selectedCameraId} audioDevice:${selectedAudioInputId}`
); );
const devicesConfig = { localStream = getBlackSilenceMediaStream();
audio: {
deviceId: { exact: selectedAudioInputId },
echoCancellation: true,
},
// we don't need a video stream on start
video: false,
};
localStream = await navigator.mediaDevices.getUserMedia(devicesConfig);
localStream.getTracks().map(track => { localStream.getTracks().map(track => {
if (localStream) { if (localStream) {
peerConnection?.addTrack(track, localStream); peerConnection?.addTrack(track, localStream);
@ -420,7 +447,6 @@ async function openMediaDevicesAndAddTracks() {
callVideoListeners(); callVideoListeners();
} }
// tslint:disable-next-line: function-name
export async function USER_callRecipient(recipient: string) { export async function USER_callRecipient(recipient: string) {
if (!getCallMediaPermissionsSettings()) { if (!getCallMediaPermissionsSettings()) {
ToastUtils.pushVideoCallPermissionNeeded(); ToastUtils.pushVideoCallPermissionNeeded();
@ -457,6 +483,7 @@ export async function USER_callRecipient(recipient: string) {
await openMediaDevicesAndAddTracks(); await openMediaDevicesAndAddTracks();
setIsRinging(true); setIsRinging(true);
await createOfferAndSendIt(recipient);
} }
const iceCandidates: Array<RTCIceCandidate> = new Array(); const iceCandidates: Array<RTCIceCandidate> = new Array();
@ -579,7 +606,6 @@ function closeVideoCall() {
window.inboxStore?.dispatch(endCall()); window.inboxStore?.dispatch(endCall());
remoteVideoStreamIsMuted = true; remoteVideoStreamIsMuted = true;
timestampAcceptedCall = undefined;
makingOffer = false; makingOffer = false;
ignoreOffer = false; ignoreOffer = false;
@ -626,7 +652,7 @@ function onDataChannelOnOpen() {
sendVideoStatusViaDataChannel(); sendVideoStatusViaDataChannel();
} }
function createOrGetPeerConnection(withPubkey: string, isAcceptingCall = false) { function createOrGetPeerConnection(withPubkey: string) {
if (peerConnection) { if (peerConnection) {
return peerConnection; return peerConnection;
} }
@ -640,21 +666,7 @@ function createOrGetPeerConnection(withPubkey: string, isAcceptingCall = false)
dataChannel.onmessage = onDataChannelReceivedMessage; dataChannel.onmessage = onDataChannelReceivedMessage;
dataChannel.onopen = onDataChannelOnOpen; dataChannel.onopen = onDataChannelOnOpen;
peerConnection.onnegotiationneeded = async () => {
const shouldTriggerAnotherNeg =
isAcceptingCall && timestampAcceptedCall && Date.now() - timestampAcceptedCall > 1000;
if (!isAcceptingCall || shouldTriggerAnotherNeg) {
await handleNegotiationNeededEvent(withPubkey);
} else {
window.log.info(
'should negotaite again but we accepted the call recently, so swallowing this one'
);
}
};
peerConnection.onsignalingstatechange = handleSignalingStateChangeEvent; peerConnection.onsignalingstatechange = handleSignalingStateChangeEvent;
peerConnection.ontrack = event => { peerConnection.ontrack = event => {
event.track.onunmute = () => { event.track.onunmute = () => {
remoteStream?.addTrack(event.track); remoteStream?.addTrack(event.track);
@ -694,9 +706,6 @@ function createOrGetPeerConnection(withPubkey: string, isAcceptingCall = false)
return peerConnection; return peerConnection;
} }
let timestampAcceptedCall: number | undefined;
// tslint:disable-next-line: function-name
export async function USER_acceptIncomingCallRequest(fromSender: string) { export async function USER_acceptIncomingCallRequest(fromSender: string) {
window.log.info('USER_acceptIncomingCallRequest'); window.log.info('USER_acceptIncomingCallRequest');
setIsRinging(false); setIsRinging(false);
@ -730,8 +739,7 @@ export async function USER_acceptIncomingCallRequest(fromSender: string) {
} }
currentCallUUID = lastOfferMessage.uuid; currentCallUUID = lastOfferMessage.uuid;
timestampAcceptedCall = Date.now(); peerConnection = createOrGetPeerConnection(fromSender);
peerConnection = createOrGetPeerConnection(fromSender, true);
await openMediaDevicesAndAddTracks(); await openMediaDevicesAndAddTracks();
@ -783,7 +791,6 @@ export async function rejectCallAlreadyAnotherCall(fromSender: string, forcedUUI
clearCallCacheFromPubkeyAndUUID(fromSender, forcedUUID); clearCallCacheFromPubkeyAndUUID(fromSender, forcedUUID);
} }
// tslint:disable-next-line: function-name
export async function USER_rejectIncomingCallRequest(fromSender: string) { export async function USER_rejectIncomingCallRequest(fromSender: string) {
setIsRinging(false); setIsRinging(false);
// close the popup call // close the popup call
@ -822,7 +829,6 @@ async function sendCallMessageAndSync(callmessage: CallMessage, user: string) {
]); ]);
} }
// tslint:disable-next-line: function-name
export async function USER_hangup(fromSender: string) { export async function USER_hangup(fromSender: string) {
window.log.info('USER_hangup'); window.log.info('USER_hangup');
@ -889,16 +895,12 @@ async function buildAnswerAndSendIt(sender: string) {
window.log.warn('cannot send answer without a currentCallUUID'); window.log.warn('cannot send answer without a currentCallUUID');
return; return;
} }
await (peerConnection as any).setLocalDescription();
const answer = await peerConnection.createAnswer({ const answer = peerConnection.localDescription;
offerToReceiveAudio: true,
offerToReceiveVideo: true,
});
if (!answer?.sdp || answer.sdp.length === 0) { if (!answer?.sdp || answer.sdp.length === 0) {
window.log.warn('failed to create answer'); window.log.warn('failed to create answer');
return; return;
} }
await peerConnection.setLocalDescription(answer);
const answerSdp = answer.sdp; const answerSdp = answer.sdp;
const callAnswerMessage = new CallMessage({ const callAnswerMessage = new CallMessage({
timestamp: Date.now(), timestamp: Date.now(),
@ -955,25 +957,26 @@ export async function handleCallTypeOffer(
!makingOffer && (peerConnection?.signalingState === 'stable' || isSettingRemoteAnswerPending); !makingOffer && (peerConnection?.signalingState === 'stable' || isSettingRemoteAnswerPending);
const polite = lastOutgoingOfferTimestamp < incomingOfferTimestamp; const polite = lastOutgoingOfferTimestamp < incomingOfferTimestamp;
const offerCollision = !readyForOffer; const offerCollision = !readyForOffer;
ignoreOffer = !polite && offerCollision; ignoreOffer = !polite && offerCollision;
if (ignoreOffer) { if (ignoreOffer) {
window.log?.warn('Received offer when unready for offer; Ignoring offer.'); window.log?.warn('Received offer when unready for offer; Ignoring offer.');
return; return;
} }
if (remoteCallUUID === currentCallUUID && currentCallUUID) { if (peerConnection && remoteCallUUID === currentCallUUID && currentCallUUID) {
window.log.info('Got a new offer message from our ongoing call'); window.log.info('Got a new offer message from our ongoing call');
isSettingRemoteAnswerPending = false;
const remoteDesc = new RTCSessionDescription({ const remoteOfferDesc = new RTCSessionDescription({
type: 'offer', type: 'offer',
sdp: callMessage.sdps[0], sdp: callMessage.sdps[0],
}); });
isSettingRemoteAnswerPending = false; isSettingRemoteAnswerPending = false;
if (peerConnection) {
await peerConnection.setRemoteDescription(remoteDesc); // SRD rolls back as needed await peerConnection.setRemoteDescription(remoteOfferDesc); // SRD rolls back as needed
await buildAnswerAndSendIt(sender); isSettingRemoteAnswerPending = false;
}
await buildAnswerAndSendIt(sender);
} else { } else {
window.inboxStore?.dispatch(incomingCall({ pubkey: sender })); window.inboxStore?.dispatch(incomingCall({ pubkey: sender }));
@ -1103,19 +1106,21 @@ export async function handleCallTypeAnswer(sender: string, callMessage: SignalSe
pubkey: sender, pubkey: sender,
}) })
); );
const remoteDesc = new RTCSessionDescription({
type: 'answer',
sdp: callMessage.sdps[0],
});
// window.log?.info('Setting remote answer pending');
isSettingRemoteAnswerPending = true;
try { try {
isSettingRemoteAnswerPending = true;
const remoteDesc = new RTCSessionDescription({
type: 'answer',
sdp: callMessage.sdps[0],
});
await peerConnection?.setRemoteDescription(remoteDesc); // SRD rolls back as needed await peerConnection?.setRemoteDescription(remoteDesc); // SRD rolls back as needed
} catch (e) { } catch (e) {
window.log.warn('setRemoteDescription failed:', e); window.log.warn('setRemoteDescriptio failed:', e);
} finally {
isSettingRemoteAnswerPending = false;
} }
isSettingRemoteAnswerPending = false;
} }
export async function handleCallTypeIceCandidates( export async function handleCallTypeIceCandidates(

@ -92,6 +92,7 @@ const callSlice = createSlice({
// only set in full screen if we have an ongoing call // only set in full screen if we have an ongoing call
if (state.ongoingWith && state.ongoingCallStatus === 'ongoing' && action.payload) { if (state.ongoingWith && state.ongoingCallStatus === 'ongoing' && action.payload) {
state.callIsInFullScreen = true; state.callIsInFullScreen = true;
return state;
} }
state.callIsInFullScreen = false; state.callIsInFullScreen = false;
return state; return state;

Loading…
Cancel
Save