diff --git a/ACKNOWLEDGMENTS.md b/ACKNOWLEDGMENTS.md index 735ec23d15..34a8d05c3b 100644 --- a/ACKNOWLEDGMENTS.md +++ b/ACKNOWLEDGMENTS.md @@ -14719,7 +14719,7 @@ For more information on this, and how to apply and follow the GNU AGPL, see ``` -## libsignal-account-keys 0.1.0, libsignal-core 0.1.0, mrp 2.59.4, protobuf 2.59.4, ringrtc 2.59.4, regex-aot 0.1.0, partial-default-derive 0.1.0 +## libsignal-account-keys 0.1.0, libsignal-core 0.1.0, mrp 2.60.1, protobuf 2.60.1, ringrtc 2.60.1, regex-aot 0.1.0, partial-default-derive 0.1.0 ``` GNU AFFERO GENERAL PUBLIC LICENSE @@ -17520,6 +17520,33 @@ SOFTWARE. ``` +## strum 0.27.2, strum_macros 0.27.2 + +``` +MIT License + +Copyright (c) 2019 Peter Glotfelty + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +``` + ## zeroize_derive 1.4.2 ``` diff --git a/package.json b/package.json index 226b1a0dba..88cd88ffd6 100644 --- a/package.json +++ b/package.json @@ -137,7 +137,7 @@ "@signalapp/minimask": "1.0.1", "@signalapp/mute-state-change": "workspace:1.0.0", "@signalapp/quill-cjs": "2.1.2", - "@signalapp/ringrtc": "2.59.4", + "@signalapp/ringrtc": "2.60.1", "@signalapp/sqlcipher": "2.4.4", "@signalapp/windows-ucv": "1.0.1", "@tanstack/react-virtual": "3.11.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 08a469624e..98589e3cff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -136,8 +136,8 @@ importers: specifier: 2.1.2 version: 2.1.2 '@signalapp/ringrtc': - specifier: 2.59.4 - version: 2.59.4 + specifier: 2.60.1 + version: 2.60.1 '@signalapp/sqlcipher': specifier: 2.4.4 version: 2.4.4 @@ -3506,8 +3506,8 @@ packages: resolution: {integrity: sha512-y2sgqdivlrG41J4Zvt/82xtH/PZjDlgItqlD2g/Cv3ZbjlR6cGhTNXbfNygCJB8nXj+C7I28pjt1Zm3k0pv2mg==} engines: {npm: '>=8.2.3'} - '@signalapp/ringrtc@2.59.4': - resolution: {integrity: sha512-Ml9ArS8gi8RJvdat0AuLXggGWMGkOsBizKkz2MtI5XALK0o09wh0r/Qdr0fmp4ewJqnqWGJ31tfr+gXoelG8gQ==} + '@signalapp/ringrtc@2.60.1': + resolution: {integrity: sha512-+gqJsAnIVHf4kt+GzFkD+BSk5i/O/+vZNixcB/I90E+EvhEeQl7WJCp/54slbzybHghNRwuMUSoug/o0XrUf1A==} hasBin: true '@signalapp/sqlcipher@2.4.4': @@ -14324,7 +14324,7 @@ snapshots: lodash: 4.17.21 quill-delta: 5.1.0 - '@signalapp/ringrtc@2.59.4': + '@signalapp/ringrtc@2.60.1': dependencies: https-proxy-agent: 7.0.6 tar: 6.2.1 diff --git a/ts/services/calling.preload.ts b/ts/services/calling.preload.ts index a508076ba3..15cd081748 100644 --- a/ts/services/calling.preload.ts +++ b/ts/services/calling.preload.ts @@ -22,6 +22,8 @@ import { CallLinkEpoch, CallLogLevel, CallState, + CallEndReason, + CallRejectReason, ConnectionState, DataMode, JoinState, @@ -67,13 +69,13 @@ import { isMe } from '../util/whatTypeOfConversation.dom.js'; import { getAbsoluteTempPath } from '../util/migrations.preload.js'; import type { AvailableIODevicesType, - CallEndedReason, IceServerType, IceServerCacheType, MediaDeviceSettings, PresentedSource, } from '../types/Calling.std.js'; import { + CallEndedReason, GroupCallConnectionState, GroupCallJoinState, ScreenShareStatus, @@ -132,7 +134,6 @@ import { formatLocalDeviceState, formatPeekInfo, getPeerIdFromConversation, - getLocalCallEventFromCallEndedReason, getCallDetailsFromEndedDirectCall, getCallEventDetails, getLocalCallEventFromJoinState, @@ -528,8 +529,8 @@ export class CallingClass { this.#handleOutputDeviceChanged.bind(this); RingRTC.handleInputDeviceChanged = this.#handleInputDeviceChanged.bind(this); - RingRTC.handleAutoEndedIncomingCallRequest = - this.#handleAutoEndedIncomingCallRequest.bind(this); + RingRTC.handleRejectedIncomingCallRequest = + this.#handleRejectedIncomingCallRequest.bind(this); RingRTC.handleLogMessage = this.#handleLogMessage.bind(this); RingRTC.handleSendHttpRequest = this.#handleSendHttpRequest.bind(this); RingRTC.handleSendCallMessage = this.#handleSendCallMessage.bind(this); @@ -710,6 +711,8 @@ export class CallingClass { ); enableLocalCameraIfNecessary(); + RingRTC.setMicrophoneWarmupEnabled(hasLocalAudio); + log.info(`${logId}: Returning direct call`); return { callMode: CallMode.Direct, @@ -736,6 +739,8 @@ export class CallingClass { groupCall.setOutgoingAudioMuted(!hasLocalAudio); groupCall.setOutgoingVideoMuted(!hasLocalVideo); + RingRTC.setMicrophoneWarmupEnabled(hasLocalAudio); + enableLocalCameraIfNecessary(); log.info(`${logId}: Returning group call`); @@ -760,6 +765,8 @@ export class CallingClass { this.#stopDeviceReselectionTimer(); this.#lastMediaDeviceSettings = undefined; + RingRTC.setMicrophoneWarmupEnabled(false); + if (conversationId) { this.#getGroupCall(conversationId)?.disconnect(); } @@ -1066,6 +1073,8 @@ export class CallingClass { groupCall.setOutgoingAudioMuted(!hasLocalAudio); groupCall.setOutgoingVideoMuted(!hasLocalVideo); + RingRTC.setMicrophoneWarmupEnabled(hasLocalAudio); + if (hasLocalVideo) { drop(this.enableLocalCamera(CallMode.Group)); } @@ -1706,7 +1715,7 @@ export class CallingClass { requestGroupMembers: groupCall => { groupCall.setGroupMembers(this.#getGroupCallMembers(conversationId)); }, - onEnded: (groupCall, endedReason) => { + onEnded: (groupCall, endedReason, _summary) => { const localDeviceState = groupCall.getLocalDeviceState(); const peekInfo = groupCall.getPeekInfo(); @@ -1717,6 +1726,8 @@ export class CallingClass { peekInfo ? formatPeekInfo(peekInfo) : '(No PeekInfo)' ); + // TODO: handle call summary + this.#reduxInterface?.groupCallEnded({ conversationId, endedReason, @@ -1771,6 +1782,14 @@ export class CallingClass { this.#reduxInterface?.joinedAdhocCall(peerId); drop(this.#sendProfileKeysForAdhocCall({ roomId: peerId, peekInfo })); } + + // If it's just us, warm up + const call = this.#getGroupCall(peerId); + if (call?.getRemoteDeviceStates()?.length === 0) { + RingRTC.setMicrophoneWarmupEnabled( + !call?.getLocalDeviceState()?.audioMuted + ); + } } async #sendProfileKeysForAdhocCall({ @@ -1965,6 +1984,77 @@ export class CallingClass { } } + #convertRingRtcCallRejectReason( + rejectReason: CallRejectReason + ): CallEndedReason { + switch (rejectReason) { + case CallRejectReason.GlareHandlingFailure: + return CallEndedReason.GlareFailure; + case CallRejectReason.ReceivedOfferExpired: + return CallEndedReason.ReceivedOfferExpired; + case CallRejectReason.ReceivedOfferWithGlare: + return CallEndedReason.ReceivedOfferWithGlare; + case CallRejectReason.ReceivedOfferWhileActive: + return CallEndedReason.ReceivedOfferWhileActive; + default: + throw missingCaseError(rejectReason); + } + } + + #convertRingRtcDirectCallEndReason( + callEndReason: CallEndReason + ): CallEndedReason { + switch (callEndReason) { + case CallEndReason.LocalHangup: + return CallEndedReason.LocalHangup; + case CallEndReason.RemoteHangup: + return CallEndedReason.RemoteHangup; + case CallEndReason.RemoteHangupNeedPermission: + return CallEndedReason.RemoteHangupNeedPermission; + case CallEndReason.RemoteHangupAccepted: + return CallEndedReason.AcceptedOnAnotherDevice; + case CallEndReason.RemoteHangupDeclined: + return CallEndedReason.DeclinedOnAnotherDevice; + case CallEndReason.RemoteHangupBusy: + return CallEndedReason.BusyOnAnotherDevice; + case CallEndReason.RemoteBusy: + return CallEndedReason.Busy; + case CallEndReason.RemoteGlare: + return CallEndedReason.Glare; + case CallEndReason.RemoteReCall: + return CallEndedReason.ReCall; + case CallEndReason.Timeout: + return CallEndedReason.Timeout; + case CallEndReason.InternalFailure: + return CallEndedReason.InternalFailure; + case CallEndReason.SignalingFailure: + return CallEndedReason.SignalingFailure; + case CallEndReason.ConnectionFailure: + return CallEndedReason.ConnectionFailure; + // The rest of the values are unexpected in this context. + case CallEndReason.AppDroppedCall: + case CallEndReason.DeviceExplicitlyDisconnected: + case CallEndReason.ServerExplicitlyDisconnected: + case CallEndReason.DeniedRequestToJoinCall: + case CallEndReason.RemovedFromCall: + case CallEndReason.CallManagerIsBusy: + case CallEndReason.SfuClientFailedToJoin: + case CallEndReason.FailedToCreatePeerConnectionFactory: + case CallEndReason.FailedToNegotiatedSrtpKeys: + case CallEndReason.FailedToCreatePeerConnection: + case CallEndReason.FailedToStartPeerConnection: + case CallEndReason.FailedToUpdatePeerConnection: + case CallEndReason.FailedToSetMaxSendBitrate: + case CallEndReason.IceFailedWhileConnecting: + case CallEndReason.IceFailedAfterConnected: + case CallEndReason.ServerChangedDemuxId: + case CallEndReason.HasMaxDevices: + return CallEndedReason.UnexpectedReason; + default: + throw missingCaseError(callEndReason); + } + } + // See the comment in types/Calling.ts to explain why we have to do this conversion. #convertRingRtcJoinState(joinState: JoinState): GroupCallJoinState { switch (joinState) { @@ -2327,6 +2417,7 @@ export class CallingClass { this.videoRenderer.disable(); call.setOutgoingAudioMuted(true); call.setOutgoingVideoMuted(true); + RingRTC.setMicrophoneWarmupEnabled(false); if ( excludeRinging && @@ -2341,6 +2432,7 @@ export class CallingClass { // This ensures that we turn off our devices. call.setOutgoingAudioMuted(true); call.setOutgoingVideoMuted(true); + RingRTC.setMicrophoneWarmupEnabled(false); call.disconnect(); } else { throw missingCaseError(call); @@ -3401,10 +3493,10 @@ export class CallingClass { } } - async #handleAutoEndedIncomingCallRequest( + async #handleRejectedIncomingCallRequest( callIdValue: CallId, remoteUserId: UserId, - callEndedReason: CallEndedReason, + callRejectReason: CallRejectReason, ageInSeconds: number, wasVideoCall: boolean, receivedAtCounter: number | undefined, @@ -3412,7 +3504,7 @@ export class CallingClass { ) { const conversation = window.ConversationController.get(remoteUserId); if (!conversation) { - log.warn('handleAutoEndedIncomingCallRequest: Conversation not found'); + log.warn('handleRejectedIncomingCallRequest: Conversation not found'); return; } @@ -3422,6 +3514,20 @@ export class CallingClass { }); log.info(logId); + if (callRejectReason === CallRejectReason.ReceivedOfferWhileActive) { + // This is a special case where we won't update our local call, because we have + // an ongoing active call. The ended call would stomp on the active call. + log.info( + `${logId}: Got offer while active for conversation ${conversation?.idForLogging()}` + ); + return; + } + + // Attempt to translate the rejection reason to its CallEndedReason + // counterpart. + const callEndedReason = + this.#convertRingRtcCallRejectReason(callRejectReason); + const callId = Long.fromValue(callIdValue).toString(); const peerId = getPeerIdFromConversation(conversation.attributes); @@ -3440,17 +3546,18 @@ export class CallingClass { wasVideoCall, timestamp ); - const localCallEvent = - getLocalCallEventFromCallEndedReason(callEndedReason); + + // We classify all of the call rejection events as 'Missed'. const callEvent = getCallEventDetails( callDetails, - localCallEvent, - 'CallingClass.handleAutoEndedIncomingCallRequest' + LocalCallEvent.Missed, + 'CallingClass.handleRejectedIncomingCallRequest' ); if (!this.#reduxInterface) { log.error(`${logId}: Unable to update redux for call`); } + this.#reduxInterface?.callStateChange({ acceptedTime: null, callEndedReason, @@ -3504,7 +3611,15 @@ export class CallingClass { delete this.#callsLookup[conversationId]; } - const localCallEvent = getLocalCallEventFromDirectCall(call); + const callEndedReason = + call.endedReason != null + ? this.#convertRingRtcDirectCallEndReason(call.endedReason) + : undefined; + + const localCallEvent = getLocalCallEventFromDirectCall( + call, + callEndedReason + ); if (localCallEvent != null) { const peerId = getPeerIdFromConversation(conversation.attributes); const callDetails = getCallDetailsFromDirectCall(peerId, call); @@ -3516,10 +3631,12 @@ export class CallingClass { await updateCallHistoryFromLocalEvent(callEvent, null, null); } + // TODO: handle CallSummary here. + reduxInterface.callStateChange({ conversationId, callState: call.state, - callEndedReason: call.endedReason, + callEndedReason, acceptedTime, }); }; diff --git a/ts/state/ducks/calling.preload.ts b/ts/state/ducks/calling.preload.ts index 788685ae06..4700f6f55e 100644 --- a/ts/state/ducks/calling.preload.ts +++ b/ts/state/ducks/calling.preload.ts @@ -7,7 +7,7 @@ import type { ReadonlyDeep } from 'type-fest'; import { CallLinkEpoch, CallLinkRootKey, - GroupCallEndReason, + CallEndReason, type Reaction as CallReaction, } from '@signalapp/ringrtc'; import { getOwn } from '../../util/getOwn.std.js'; @@ -799,7 +799,7 @@ type DirectCallAudioLevelsChangeActionType = ReadonlyDeep<{ type GroupCallEndedActionPayloadType = ReadonlyDeep<{ conversationId: string; - endedReason: GroupCallEndReason; + endedReason: CallEndReason; }>; export type GroupCallEndedActionType = ReadonlyDeep<{ @@ -1256,18 +1256,7 @@ function callStateChange( CallStateChangeFulfilledActionType > { return async dispatch => { - const { conversationId, callState, acceptedTime, callEndedReason } = - payload; - - // This is a special case were we won't update our local call, because we have an - // ongoing active call. The ended call would stomp on the active call. - if (callEndedReason === CallEndedReason.ReceivedOfferWhileActive) { - const conversation = window.ConversationController.get(conversationId); - log.info( - `callStateChange: Got offer while active for conversation ${conversation?.idForLogging()}` - ); - return; - } + const { callState, acceptedTime, callEndedReason } = payload; const wasAccepted = acceptedTime != null; const isEnded = callState === CallState.Ended && callEndedReason != null; @@ -1485,7 +1474,7 @@ function groupCallEnded( > { return (dispatch, getState) => { const { endedReason } = payload; - if (endedReason === GroupCallEndReason.DeniedRequestToJoinCall) { + if (endedReason === CallEndReason.DeniedRequestToJoinCall) { const i18n = getIntl(getState()); dispatch({ type: SHOW_ERROR_MODAL, @@ -1497,7 +1486,7 @@ function groupCallEnded( }); return; } - if (endedReason === GroupCallEndReason.RemovedFromCall) { + if (endedReason === CallEndReason.RemovedFromCall) { const i18n = getIntl(getState()); dispatch({ type: SHOW_ERROR_MODAL, @@ -1509,7 +1498,7 @@ function groupCallEnded( }); return; } - if (endedReason === GroupCallEndReason.HasMaxDevices) { + if (endedReason === CallEndReason.HasMaxDevices) { const i18n = getIntl(getState()); dispatch({ type: SHOW_ERROR_MODAL, diff --git a/ts/types/Calling.std.ts b/ts/types/Calling.std.ts index c8cd27c53b..679f9851a3 100644 --- a/ts/types/Calling.std.ts +++ b/ts/types/Calling.std.ts @@ -124,7 +124,6 @@ export enum CallState { Ended = 'ended', } -// Must be kept in sync with RingRTC.CallEndedReason export enum CallEndedReason { LocalHangup = 'LocalHangup', RemoteHangup = 'RemoteHangup', @@ -144,6 +143,7 @@ export enum CallEndedReason { AcceptedOnAnotherDevice = 'AcceptedOnAnotherDevice', DeclinedOnAnotherDevice = 'DeclinedOnAnotherDevice', BusyOnAnotherDevice = 'BusyOnAnotherDevice', + UnexpectedReason = 'UnexpectedReason', } // Must be kept in sync with RingRTC's ConnectionState diff --git a/ts/util/callDisposition.preload.ts b/ts/util/callDisposition.preload.ts index 3154d2fe64..275e423b43 100644 --- a/ts/util/callDisposition.preload.ts +++ b/ts/util/callDisposition.preload.ts @@ -427,6 +427,7 @@ const endedReasonToEvent: Record = { [CallEndedReason.Timeout]: LocalCallEvent.Missed, [CallEndedReason.Declined]: LocalCallEvent.Missed, [CallEndedReason.DeclinedOnAnotherDevice]: LocalCallEvent.Missed, + [CallEndedReason.UnexpectedReason]: LocalCallEvent.Missed, }; export function getLocalCallEventFromCallEndedReason( @@ -437,15 +438,16 @@ export function getLocalCallEventFromCallEndedReason( } export function getLocalCallEventFromDirectCall( - call: Call + call: Call, + callEndedReason: CallEndedReason | undefined ): LocalCallEvent | null { log.info('getLocalCallEventFromDirectCall', call.state); if (call.state === CallState.Accepted) { return LocalCallEvent.Accepted; } if (call.state === CallState.Ended) { - strictAssert(call.endedReason != null, 'Call ended without reason'); - return getLocalCallEventFromCallEndedReason(call.endedReason); + strictAssert(callEndedReason, 'Call ended without reason'); + return getLocalCallEventFromCallEndedReason(callEndedReason); } if (call.state === CallState.Ringing) { return LocalCallEvent.Ringing;