From 076b47e69539494ecf2ca2308006294e5c53e5d7 Mon Sep 17 00:00:00 2001 From: Jim Gustafson Date: Tue, 18 Mar 2025 03:50:54 -0700 Subject: [PATCH] Improve calling reliability with relay server response cache. Co-authored-by: Cody Henthorne --- .../service/webrtc/SignalCallManager.java | 41 ++++++++++++++----- .../service/webrtc/TurnServerCache.kt | 41 +++++++++++++++++++ .../api/messages/calls/TurnServerInfo.java | 8 +++- 3 files changed, 79 insertions(+), 11 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/service/webrtc/TurnServerCache.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java index 5e7d790e2f..5086e61a15 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java @@ -1033,17 +1033,26 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall. public void retrieveTurnServers(@NonNull RemotePeer remotePeer) { networkExecutor.execute(() -> { try { - List turnServerInfos = NetworkResultUtil.toBasicLegacy(SignalNetwork.calling().getTurnServerInfo()); - List iceServers = mapToIceServers(turnServerInfos); - process((s, p) -> { - RemotePeer activePeer = s.getCallInfoState().getActivePeer(); - if (activePeer != null && activePeer.getCallId().equals(remotePeer.getCallId())) { - return p.handleTurnServerUpdate(s, iceServers, TextSecurePreferences.isTurnOnly(context)); - } + List cachedServers = TurnServerCache.getCachedServers(); + if (cachedServers != null) { + processTurnServers(remotePeer, cachedServers); + return; + } - Log.w(TAG, "Ignoring received turn servers for incorrect call id. requesting_call_id: " + remotePeer.getCallId() + " current_call_id: " + (activePeer != null ? activePeer.getCallId() : "null")); - return s; - }); + List turnServerInfos = NetworkResultUtil.toBasicLegacy(SignalNetwork.calling().getTurnServerInfo()); + + // Find *any* provided ttl values as long as they are valid. + long minTtl = turnServerInfos.stream() + .map(TurnServerInfo::getTtl) + .filter(ttl -> ttl != null && ttl > 0) + .min(Long::compare) + .orElse(0L); + + List iceServers = mapToIceServers(turnServerInfos); + + TurnServerCache.updateCache(iceServers, minTtl); + + processTurnServers(remotePeer, iceServers); } catch (IOException e) { Log.w(TAG, "Unable to retrieve turn servers: ", e); process((s, p) -> p.handleSetupFailure(s, remotePeer.getCallId())); @@ -1051,6 +1060,18 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall. }); } + private void processTurnServers(RemotePeer remotePeer, List servers) { + process((s, p) -> { + RemotePeer activePeer = s.getCallInfoState().getActivePeer(); + if (activePeer != null && activePeer.getCallId().equals(remotePeer.getCallId())) { + return p.handleTurnServerUpdate(s, servers, TextSecurePreferences.isTurnOnly(context)); + } + + Log.w(TAG, "Ignoring received turn servers for incorrect call id. requesting_call_id: " + remotePeer.getCallId() + " current_call_id: " + (activePeer != null ? activePeer.getCallId() : "null")); + return s; + }); + } + private static List mapToIceServers(@NonNull List turnServerInfos) { List iceServers = new ArrayList<>(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/TurnServerCache.kt b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/TurnServerCache.kt new file mode 100644 index 0000000000..824ba7a783 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/TurnServerCache.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.service.webrtc + +import android.os.SystemClock +import org.webrtc.PeerConnection +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds + +/** + * Provide an in-memory cache of TURN servers used for calling as the endpoint + * is rate limited and the data should be valid for the duration of a app run. + */ +object TurnServerCache { + private var iceServers: List? = null + private var lastUpdated: Duration = Duration.ZERO + private var cacheTtl: Duration = Duration.ZERO + + @JvmStatic + fun getCachedServers(): List? { + val now = SystemClock.elapsedRealtime().milliseconds + + return if (iceServers != null && now > lastUpdated && now < (lastUpdated + cacheTtl)) { + iceServers + } else { + null + } + } + + @JvmStatic + fun updateCache(newServers: List, ttl: Long) { + lastUpdated = SystemClock.elapsedRealtime().milliseconds + cacheTtl = ttl.seconds + + iceServers = newServers + } +} diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/messages/calls/TurnServerInfo.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/messages/calls/TurnServerInfo.java index 5833d512c7..ef045c0e9b 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/messages/calls/TurnServerInfo.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/messages/calls/TurnServerInfo.java @@ -1,6 +1,5 @@ package org.whispersystems.signalservice.api.messages.calls; - import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; @@ -22,6 +21,9 @@ public class TurnServerInfo { @JsonProperty private List urlsWithIps; + @JsonProperty + private Long ttl; + public String getUsername() { return username; } @@ -42,4 +44,8 @@ public class TurnServerInfo { public List getUrlsWithIps() { return urlsWithIps; } + + public Long getTtl() { + return ttl; + }; }