From ec51d0dd0c3dfb4183300c0faad70888b5dc575e Mon Sep 17 00:00:00 2001 From: thughari Date: Mon, 6 Oct 2025 13:37:00 +0530 Subject: [PATCH] updated auto turn --- .dockerignore | 41 ++++++ pom.xml | 6 + .../randomchat/RandomChatApplication.java | 8 -- .../component/TwilioHttpClient.java | 83 ++++++++++++ .../config/VirtualThreadConfig.java | 16 +++ .../controller/TurnConfigController.java | 33 +++-- .../exceptions/TwilioClientException.java | 14 ++ .../randomchat/handler/SignalingHandler.java | 1 + .../randomchat/service/TwilioTurnService.java | 121 ++++++++++++++++++ src/main/resources/application-dev.properties | 4 +- .../resources/application-prod.properties | 7 +- src/main/resources/static/script.js | 26 +--- 12 files changed, 309 insertions(+), 51 deletions(-) create mode 100644 .dockerignore create mode 100644 src/main/java/com/thughari/randomchat/component/TwilioHttpClient.java create mode 100644 src/main/java/com/thughari/randomchat/config/VirtualThreadConfig.java create mode 100644 src/main/java/com/thughari/randomchat/exceptions/TwilioClientException.java create mode 100644 src/main/java/com/thughari/randomchat/service/TwilioTurnService.java diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a5bcda7 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,41 @@ +# IDE and build system files +.idea/ +.vscode/ +*.iml +*.log +*.tmp +*.swp +*.swo +*.bak +*.orig +*.rej + +# Maven and Eclipse files +.mvn/ +.settings/ +.classpath +.project +.factorypath +target/ +**/target/ +**/.classpath +**/.project +**/.settings + +# Git files +.git/ +.gitignore +.gitattributes + +# OS/Editor junk +.DS_Store +Thumbs.db + +# Test sources +src/test/ +**/src/test/ + +# Docs and markdown +README.md +CONTRIBUTING.md +HELP.md \ No newline at end of file diff --git a/pom.xml b/pom.xml index d0942bb..a1ccaac 100644 --- a/pom.xml +++ b/pom.xml @@ -50,6 +50,12 @@ lombok true + + + org.springframework.boot + spring-boot-starter-webflux + + org.springframework.boot spring-boot-starter-test diff --git a/src/main/java/com/thughari/randomchat/RandomChatApplication.java b/src/main/java/com/thughari/randomchat/RandomChatApplication.java index d9e1c54..7ebbf66 100644 --- a/src/main/java/com/thughari/randomchat/RandomChatApplication.java +++ b/src/main/java/com/thughari/randomchat/RandomChatApplication.java @@ -1,11 +1,7 @@ package com.thughari.randomchat; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; import org.springframework.scheduling.annotation.EnableAsync; @SpringBootApplication @@ -16,8 +12,4 @@ public static void main(String[] args) { SpringApplication.run(RandomChatApplication.class, args); } - @Bean("virtualThreadTaskExecutor") - public ExecutorService virtualThreadTaskExecutor() { - return Executors.newVirtualThreadPerTaskExecutor(); - } } \ No newline at end of file diff --git a/src/main/java/com/thughari/randomchat/component/TwilioHttpClient.java b/src/main/java/com/thughari/randomchat/component/TwilioHttpClient.java new file mode 100644 index 0000000..f2d38f0 --- /dev/null +++ b/src/main/java/com/thughari/randomchat/component/TwilioHttpClient.java @@ -0,0 +1,83 @@ +package com.thughari.randomchat.component; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import com.thughari.randomchat.exceptions.TwilioClientException; + +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.client.ResourceAccessException; + +import java.util.Base64; + +@Component +public class TwilioHttpClient { + + private static final Logger logger = LoggerFactory.getLogger(TwilioHttpClient.class); + + @Value("${twilio.account.sid}") + private String twilioAccountSid; + + @Value("${twilio.auth.token}") + private String twilioAuthToken; + + private final RestTemplate restTemplate; + + public TwilioHttpClient() { + this.restTemplate = new RestTemplate(); + } + + /** + * Makes an HTTP POST request to Twilio's Tokens API to fetch ICE servers. + * + * @return The raw JSON response body from Twilio. + * @throws TwilioClientException if the API call fails or returns a non-successful status. + */ + public String fetchTwilioTokensApiResponse() throws TwilioClientException { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + String auth = twilioAccountSid + ":" + twilioAuthToken; + String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes()); + headers.add("Authorization", "Basic " + encodedAuth); + + HttpEntity requestEntity = new HttpEntity<>(headers); + + String twilioApiUrl = String.format("https://api.twilio.com/2010-04-01/Accounts/%s/Tokens.json", twilioAccountSid); + + try { + logger.debug("Making Twilio Tokens API call to: {}", twilioApiUrl); + ResponseEntity response = restTemplate.exchange( + twilioApiUrl, + HttpMethod.POST, + requestEntity, + String.class + ); + + if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { + logger.debug("Successfully received response from Twilio API."); + return response.getBody(); + } else { + logger.error("Twilio API call failed with status: {} and body: {}", response.getStatusCode(), response.getBody()); + throw new TwilioClientException("Twilio API returned non-2xx status: " + response.getStatusCode()); + } + } catch (HttpClientErrorException | HttpServerErrorException e) { + logger.error("Twilio API client/server error: {} - {}", e.getStatusCode(), e.getResponseBodyAsString(), e); + throw new TwilioClientException("Twilio API returned an HTTP error: " + e.getStatusCode(), e); + } catch (ResourceAccessException e) { + logger.error("Network or connection error connecting to Twilio API: {}", e.getMessage(), e); + throw new TwilioClientException("Network error connecting to Twilio API", e); + } catch (Exception e) { + logger.error("An unexpected error occurred during Twilio API call: {}", e.getMessage(), e); + throw new TwilioClientException("Unexpected error during Twilio API call", e); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/thughari/randomchat/config/VirtualThreadConfig.java b/src/main/java/com/thughari/randomchat/config/VirtualThreadConfig.java new file mode 100644 index 0000000..d1c4edf --- /dev/null +++ b/src/main/java/com/thughari/randomchat/config/VirtualThreadConfig.java @@ -0,0 +1,16 @@ +package com.thughari.randomchat.config; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class VirtualThreadConfig { + + @Bean("virtualThreadTaskExecutor") + public ExecutorService virtualThreadTaskExecutor() { + return Executors.newVirtualThreadPerTaskExecutor(); + } +} \ No newline at end of file diff --git a/src/main/java/com/thughari/randomchat/controller/TurnConfigController.java b/src/main/java/com/thughari/randomchat/controller/TurnConfigController.java index 6ff94ac..8ba2223 100644 --- a/src/main/java/com/thughari/randomchat/controller/TurnConfigController.java +++ b/src/main/java/com/thughari/randomchat/controller/TurnConfigController.java @@ -1,29 +1,28 @@ package com.thughari.randomchat.controller; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.CompletableFuture; - -import org.springframework.beans.factory.annotation.Value; +import com.thughari.randomchat.service.TwilioTurnService; import org.springframework.scheduling.annotation.Async; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + @RestController public class TurnConfigController { - @Value("${turn.server.username}") - private String username; + private final TwilioTurnService twilioTurnService; - @Value("${turn.server.credential}") - private String credential; + // Constructor injection + public TurnConfigController(TwilioTurnService twilioTurnService) { + this.twilioTurnService = twilioTurnService; + } - @GetMapping("/api/turn-config") - @Async("virtualThreadTaskExecutor") - public CompletableFuture> getTurnConfig() { - Map config = new HashMap<>(); - config.put("username", username); - config.put("credential", credential); - return CompletableFuture.completedFuture(config); - } + @GetMapping("/api/turn-config") + @Async("virtualThreadTaskExecutor") + public CompletableFuture>> getTurnConfig() { + List> iceServers = twilioTurnService.getTwilioIceServers(); + return CompletableFuture.completedFuture(iceServers); + } } \ No newline at end of file diff --git a/src/main/java/com/thughari/randomchat/exceptions/TwilioClientException.java b/src/main/java/com/thughari/randomchat/exceptions/TwilioClientException.java new file mode 100644 index 0000000..6872d64 --- /dev/null +++ b/src/main/java/com/thughari/randomchat/exceptions/TwilioClientException.java @@ -0,0 +1,14 @@ +package com.thughari.randomchat.exceptions; + +public class TwilioClientException extends Exception { + + private static final long serialVersionUID = 1L; + + public TwilioClientException(String message) { + super(message); + } + + public TwilioClientException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/src/main/java/com/thughari/randomchat/handler/SignalingHandler.java b/src/main/java/com/thughari/randomchat/handler/SignalingHandler.java index 991c870..a930432 100644 --- a/src/main/java/com/thughari/randomchat/handler/SignalingHandler.java +++ b/src/main/java/com/thughari/randomchat/handler/SignalingHandler.java @@ -24,6 +24,7 @@ @Component public class SignalingHandler extends TextWebSocketHandler { + private static final Logger logger = LoggerFactory.getLogger(SignalingHandler.class); private final Map sessions = new ConcurrentHashMap<>(); private final Queue waitingUsers = new ConcurrentLinkedQueue<>(); diff --git a/src/main/java/com/thughari/randomchat/service/TwilioTurnService.java b/src/main/java/com/thughari/randomchat/service/TwilioTurnService.java new file mode 100644 index 0000000..c2b9fba --- /dev/null +++ b/src/main/java/com/thughari/randomchat/service/TwilioTurnService.java @@ -0,0 +1,121 @@ +package com.thughari.randomchat.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.thughari.randomchat.component.TwilioHttpClient; +import com.thughari.randomchat.exceptions.TwilioClientException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Collections; // For Collections.singletonList +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +@Service +public class TwilioTurnService { + + private static final Logger logger = LoggerFactory.getLogger(TwilioTurnService.class); + + private final TwilioHttpClient twilioHttpClient; + private final ObjectMapper objectMapper = new ObjectMapper(); + + private final AtomicReference>> cachedIceServers = new AtomicReference<>(new ArrayList<>()); + private final AtomicLong cacheExpiryTime = new AtomicLong(0); + private final long CACHE_TTL_SECONDS = 60 * 60; // Cache for 1 hour + + // Google STUN server + private static final Map GOOGLE_STUN_SERVER = Map.of("urls", "stun:stun.l.google.com:19302"); + + // Constructor injection + public TwilioTurnService(TwilioHttpClient twilioHttpClient) { + this.twilioHttpClient = twilioHttpClient; + } + + public List> getTwilioIceServers() { + // Check cache first + if (!cachedIceServers.get().isEmpty() && System.currentTimeMillis() < cacheExpiryTime.get()) { + logger.info("Returning Twilio ICE servers from cache."); + return cachedIceServers.get(); + } + + logger.info("Fetching new Twilio ICE servers from API."); + return fetchNewTwilioIceServers(); + } + + private synchronized List> fetchNewTwilioIceServers() { + // Double-check locking for cache refresh + if (!cachedIceServers.get().isEmpty() && System.currentTimeMillis() < cacheExpiryTime.get()) { + logger.info("Another thread already refreshed cache, returning from cache."); + return cachedIceServers.get(); + } + + List> iceServersToReturn = new ArrayList<>(); + iceServersToReturn.add(GOOGLE_STUN_SERVER); + + try { + String responseBody = twilioHttpClient.fetchTwilioTokensApiResponse(); + + JsonNode root = objectMapper.readTree(responseBody); + JsonNode twilioIceServersNode = root.path("ice_servers"); + + if (twilioIceServersNode.isArray()) { + for (JsonNode serverNode : twilioIceServersNode) { + Map serverConfig = new HashMap<>(); + // Parse URLs + if (serverNode.has("urls")) { + if (serverNode.get("urls").isArray()) { + List urlsList = new ArrayList<>(); + for (JsonNode urlItem : serverNode.get("urls")) { + urlsList.add(urlItem.asText()); + } + serverConfig.put("urls", String.join(",", urlsList)); + } else { + serverConfig.put("urls", serverNode.get("urls").asText()); + } + } else if (serverNode.has("url")) { + serverConfig.put("urls", serverNode.get("url").asText()); + } + + if (serverConfig.containsKey("urls") && !serverConfig.get("urls").equals(GOOGLE_STUN_SERVER.get("urls"))) { + if (serverNode.has("username")) { + serverConfig.put("username", serverNode.get("username").asText()); + } + if (serverNode.has("credential")) { + serverConfig.put("credential", serverNode.get("credential").asText()); + } + iceServersToReturn.add(serverConfig); + } + } + } + + // Update cache + cachedIceServers.set(iceServersToReturn); + cacheExpiryTime.set(System.currentTimeMillis() + (CACHE_TTL_SECONDS * 1000)); + logger.info("Successfully fetched Twilio ICE servers and combined with Google STUN. Total: {} servers.", iceServersToReturn.size()); + return iceServersToReturn; + + } catch (TwilioClientException e) { + logger.error("Error communicating with Twilio API: {}", e.getMessage()); + // Fallback to Google STUN server + logger.warn("Twilio API error, returning only Google STUN server."); + updateCacheWithGoogleStunOnly(); + return Collections.singletonList(GOOGLE_STUN_SERVER); + } catch (Exception e) { + logger.error("Error processing Twilio API response or unexpected issue: {}", e.getMessage(), e); + logger.warn("Unexpected error, returning only Google STUN server."); + updateCacheWithGoogleStunOnly(); + return Collections.singletonList(GOOGLE_STUN_SERVER); + } + } + + private void updateCacheWithGoogleStunOnly() { + cachedIceServers.set(Collections.singletonList(GOOGLE_STUN_SERVER)); + cacheExpiryTime.set(System.currentTimeMillis() + (CACHE_TTL_SECONDS * 1000)); + } +} \ No newline at end of file diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index 13dbd37..a79797d 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -2,8 +2,8 @@ spring.application.name=RandomChat server.tomcat.threads.virtual.enabled=true -turn.server.username = test_username -turn.server.credential = test_credential +twilio.account.sid = test_sid +twilio.auth.token = test_token app.keepalive.url = http://localhost:8080/ping diff --git a/src/main/resources/application-prod.properties b/src/main/resources/application-prod.properties index 2d05e9f..ba2ae9b 100644 --- a/src/main/resources/application-prod.properties +++ b/src/main/resources/application-prod.properties @@ -2,8 +2,8 @@ spring.application.name=RandomChat server.tomcat.threads.virtual.enabled=true -turn.server.username = ${TURN-UNAME} -turn.server.credential = ${TURN-CREDS} +twilio.account.sid = ${twilio.account.sid} +twilio.auth.token = ${twilio.auth.token} app.keepalive.url = https://randomchat-hfta.onrender.com/ping @@ -14,3 +14,6 @@ spring.websocket.server.idle-timeout=120000 spring.web.resources.cache.cachecontrol.max-age=31536000 spring.web.resources.cache.cachecontrol.must-revalidate=true spring.web.resources.chain.strategy.content.enabled=true + +logging.level.root=WARN +logging.level.com.thughari.randomchat=DEBUG diff --git a/src/main/resources/static/script.js b/src/main/resources/static/script.js index 240c526..edd8536 100644 --- a/src/main/resources/static/script.js +++ b/src/main/resources/static/script.js @@ -163,29 +163,11 @@ async function ensurePeerConnection() { if (peerConnection && peerConnection.signalingState !== "closed") return; remoteIceCandidatesQueue = []; - const turnConfig = await fetchTurnConfig(); + const iceServersFromBackend = await fetchTurnConfig(); + console.log(iceServersFromBackend); peerConnection = new RTCPeerConnection({ - iceServers: [ - { - urls: 'stun:stun.l.google.com:19302' - }, - { - urls: "turn:global.turn.twilio.com:3478?transport=udp", - username: turnConfig.username, - credential: turnConfig.credential - }, - { - urls: "turn:global.turn.twilio.com:3478?transport=tcp", - username: turnConfig.username, - credential: turnConfig.credential - }, - { - urls: "turn:global.turn.twilio.com:443?transport=tcp", - username: turnConfig.username, - credential: turnConfig.credential - } - ] + iceServers: iceServersFromBackend }); configurePeerConnectionEventListeners(); @@ -480,6 +462,6 @@ async function fetchTurnConfig() { return cachedTurnConfig; } catch (e) { console.warn('Failed to fetch TURN config:', e); - return {}; + return []; } } \ No newline at end of file