From 58b6ae1738334cea0624fff09e2cfb3a4c3304f7 Mon Sep 17 00:00:00 2001 From: Mohammad Irfan Date: Mon, 28 Nov 2022 17:10:12 -0800 Subject: [PATCH 1/6] initial add --- .../EventgridSubscription-IncomingCall.png | Bin 0 -> 50551 bytes CallAutomation_MediaStreaming/pom.xml | 157 +++++++++++++++++ CallAutomation_MediaStreaming/readme.md | 79 +++++++++ .../com/communication/MediaStreaming/App.java | 14 ++ .../MediaStreaming/CallConfiguration.java | 29 ++++ .../MediaStreaming/ConfigurationManager.java | 43 +++++ .../Controllers/IncomingCallController.java | 96 +++++++++++ .../EventHandler/EventAuthHandler.java | 34 ++++ .../EventHandler/EventDispatcher.java | 55 ++++++ .../EventHandler/NotificationCallback.java | 7 + .../communication/MediaStreaming/Logger.java | 22 +++ .../MediaStreaming/MediaStreaming.java | 93 ++++++++++ .../MediaStreaming/config.properties | 13 ++ .../communication/WebSocketListener/App.java | 160 ++++++++++++++++++ 14 files changed, 802 insertions(+) create mode 100644 CallAutomation_MediaStreaming/media/EventgridSubscription-IncomingCall.png create mode 100644 CallAutomation_MediaStreaming/pom.xml create mode 100644 CallAutomation_MediaStreaming/readme.md create mode 100644 CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/App.java create mode 100644 CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/CallConfiguration.java create mode 100644 CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/ConfigurationManager.java create mode 100644 CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/Controllers/IncomingCallController.java create mode 100644 CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/EventHandler/EventAuthHandler.java create mode 100644 CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/EventHandler/EventDispatcher.java create mode 100644 CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/EventHandler/NotificationCallback.java create mode 100644 CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/Logger.java create mode 100644 CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/MediaStreaming.java create mode 100644 CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/config.properties create mode 100644 CallAutomation_MediaStreaming/src/main/java/com/communication/WebSocketListener/App.java diff --git a/CallAutomation_MediaStreaming/media/EventgridSubscription-IncomingCall.png b/CallAutomation_MediaStreaming/media/EventgridSubscription-IncomingCall.png new file mode 100644 index 0000000000000000000000000000000000000000..7983b61bf563ef6436d652e2c8754a6642327d09 GIT binary patch literal 50551 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 + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 2.4.4 + + + + com.communication.MediaStreaming + MediaStreaming + 1.0-SNAPSHOT + + MediaStreaming + + http://www.example.com + + + UTF-8 + 11 + 11 + + + + + junit + junit + 4.11 + test + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-test + test + + + org.apache.httpcomponents + httpclient + 4.5.13 + + + com.azure + azure-core + 1.31.0 + + + com.azure + azure-identity + 1.5.4 + + + com.azure + azure-communication-identity + 1.3.0 + + + com.azure + azure-communication-callautomation + 1.0.0-alpha.20221007.2 + + + com.microsoft.cognitiveservices.speech + client-sdk + 1.23.0 + + + com.azure + azure-messaging-eventgrid + 4.12.1 + + + com.azure + azure-cosmos + 4.34.0 + + + com.google.code.gson + gson + 2.10 + + + + + + azure-sdk-for-java + https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-java/maven/v1 + + true + + + true + + + + + + + + + maven-clean-plugin + 3.1.0 + + + maven-resources-plugin + 3.0.2 + + + maven-compiler-plugin + 3.8.0 + + + maven-surefire-plugin + 2.22.1 + + + maven-jar-plugin + 3.0.2 + + + maven-deploy-plugin + 2.8.2 + + + maven-site-plugin + 3.7.1 + + + maven-project-info-reports-plugin + 3.0.0 + + + org.codehaus.mojo + exec-maven-plugin + 3.0.0 + + + + java + + + + + com.communication.MediaStreaming.App + + + + + + diff --git a/CallAutomation_MediaStreaming/readme.md b/CallAutomation_MediaStreaming/readme.md new file mode 100644 index 0000000..af03a21 --- /dev/null +++ b/CallAutomation_MediaStreaming/readme.md @@ -0,0 +1,79 @@ +--- +page_type: sample +languages: +- java +products: +- azure +- azure-communication-services +--- + +# Incoming call Media Streaming Sample + +Get started with audio media streaming, through Azure Communication Services Call Automation SDK. +This QuickStart assumes you’ve already used the calling automation APIs to build an automated call routing solution, please refer [Call Automation IVR Sample](https://github.com/Azure-Samples/communication-services-java-quickstarts/tree/main/CallAutomation_SimpleIvr). + +In this sample a WebApp receives an incoming call request whenever a call is made to a Communication Service acquired phone number or a communication identifier. +API first answers the call with Media Streaming options settings. Once call connected, external PSTN user say something. +The audio is streamed to WebSocket server and generates log events to show media streaming is happening on the server. +It supports Audio streaming only (mixed/unmixed format). + +This sample has 3 parts: + +1. ACS Resource IncomingCall Hook Settings, and ACS acquired Phone Number. +2. IncomingCall WebApp - for accepting the incoming call with Media Options settings. +3. WebSocketListener – Listen to media stream on websocket. + +The application is a console-based application build on Java development kit(JDK) 11. + +## Getting started + +### Prerequisites + +- Create an Azure account with an active subscription. For details, see [Create an account for free](https://azure.microsoft.com/free/) +- [Java Development Kit (JDK) version 11 or above](https://docs.microsoft.com/azure/developer/java/fundamentals/java-jdk-install) +- [Apache Maven](https://maven.apache.org/download.cgi) +- Create an Azure Communication Services resource. For details, see [Create an Azure Communication Resource](https://docs.microsoft.com/azure/communication-services/quickstarts/create-communication-resource). You'll need to record your resource **connection string** for this sample. +- [Configuring the webhook](https://docs.microsoft.com/en-us/azure/devops/service-hooks/services/webhooks?view=azure-devops) for **Microsoft.Communication.IncomingCall** event. +- Download and install [Ngrok](https://www.ngrok.com/download). As the sample is run locally, Ngrok will enable the receiving of all the events. + +## Before running the sample for the first time + +1. Open an instance of PowerShell, Windows Terminal, Command Prompt or equivalent and navigate to the directory that you'd like to clone the sample to. +2. git clone https://github.com/Azure-Samples/Communication-Services-java-quickstarts.git. + +### Locally running the media streaming WebSocket app + and update below configurations. +1. Navigate to IncomingCallMediaStreaming, look for the file at /src/main/java/com/communication/IncomingCallMediaStreaming/WebSocket/App.java +2. Run the `WebSocket/App.java` for listening media stream on websocket. +3. Run ngrok using command `ngrok http 8080 --host-header="localhost:8080"` +3. Use the ngrok-URL, as a websocket URL needed for `MediaStreamingTransportURI` configuration. + +### Publish the Incoming call media streaming to Azure WebApp +1. +2. +3. After publishing, add the following configurations on azure portal (under app service's configuration section). + + - Connectionstring: Azure Communication Service resource's connection string. + - AppCallBackUri: URI of the deployed app service. + - SecretPlaceholder: Query string for callback URL. + - MediaStreamingTransportURI: websocket URL got from `WebSocketListener`, format "wss://{ngrokr-url}",(Notice the url, it should wss:// and not https://). + +### Create Webhook for incoming call event + +IncomingCall is an Azure Event Grid event for notifying incoming calls to your Communication Services resource. To learn more about it, see [this guide](https://learn.microsoft.com/en-us/azure/communication-services/concepts/call-automation/incoming-call-notification). +1. Navigate to your resource on Azure portal and select `Events` from the left side menu. + +2. Select `+ Event Subscription` to create a new subscription. +3. Filter for Incoming Call event. +4. Choose endpoint type as web hook and provide the public url generated for your application by ngrok. Make sure to provide the exact api route that you programmed to receive the event previously. In this case, it would be /api/incomingCall. + + ![Screenshot of portal page to create a new event subscription.](./media/EventgridSubscription-IncomingCall.png) + +5. Select create to start the creation of subscription and validation of your endpoint as mentioned previously. The subscription is ready when the provisioning status is marked as succeeded. + +### Run the Application + +- Navigate to the directory containing the pom.xml file and use the following mvn commands: + - Compile the application: mvn compile + - Build the package: mvn package + - Execute the app: mvn exec:java diff --git a/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/App.java b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/App.java new file mode 100644 index 0000000..7e3a237 --- /dev/null +++ b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/App.java @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.communication.MediaStreaming; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class App { + public static void main(String[] args) { + SpringApplication.run(App.class, args); + } + +} \ No newline at end of file diff --git a/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/CallConfiguration.java b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/CallConfiguration.java new file mode 100644 index 0000000..d7f3d9c --- /dev/null +++ b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/CallConfiguration.java @@ -0,0 +1,29 @@ +package com.communication.MediaStreaming; +import com.communication.MediaStreaming.EventHandler.EventAuthHandler; + +public class CallConfiguration { + public String connectionString; + public String appBaseUrl; + public String appCallbackUrl; + public String acceptCallsFrom; + public String mediaStreamingTransportURI; + + public CallConfiguration(String connectionString, String appBaseUrl, String acceptCallsFrom, String mediaStreamingTransportURI) { + this.connectionString = connectionString; + this.appBaseUrl = appBaseUrl; + EventAuthHandler eventhandler = EventAuthHandler.getInstance(); + this.appCallbackUrl = appBaseUrl + "/api/IncomingCallMediaStreaming/callback?" + eventhandler.getSecretQuerystring(); + this.acceptCallsFrom = acceptCallsFrom; + this.mediaStreamingTransportURI = mediaStreamingTransportURI; + } + + public static CallConfiguration initiateConfiguration() { + ConfigurationManager configurationManager = ConfigurationManager.getInstance(); + configurationManager.loadAppSettings(); + String connectionString = configurationManager.getAppSettings("Connectionstring"); + String acceptCallsFrom = configurationManager.getAppSettings("AcceptCallsFrom"); + String mediaStreamingTransportURI = configurationManager.getAppSettings("MediaStreamingTransportURI"); + String appBaseUrl = configurationManager.getAppSettings("AppCallBackUri"); + return new CallConfiguration(connectionString, appBaseUrl, acceptCallsFrom, mediaStreamingTransportURI); + } +} \ No newline at end of file diff --git a/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/ConfigurationManager.java b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/ConfigurationManager.java new file mode 100644 index 0000000..ad93398 --- /dev/null +++ b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/ConfigurationManager.java @@ -0,0 +1,43 @@ +package com.communication.MediaStreaming; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.util.Properties; + +public class ConfigurationManager { + private static ConfigurationManager configurationManager = null; + private final Properties appSettings = new Properties(); + + private ConfigurationManager() { + } + + // static method to create instance of ConfigurationManager class + public static ConfigurationManager getInstance() { + if (configurationManager == null) { + configurationManager = new ConfigurationManager(); + } + return configurationManager; + } + + public void loadAppSettings() { + try { + File configFile = new File("src/main/java/com/communication/MediaStreaming/config.properties"); + FileReader reader = new FileReader(configFile); + appSettings.load(reader); + reader.close(); + } catch (FileNotFoundException ex) { + Logger.logMessage(Logger.MessageType.INFORMATION,"Loading app settings failed with error -- > " + ex.getMessage()); + } catch (IOException ex) { + Logger.logMessage(Logger.MessageType.ERROR,"Loading app settings failed with error -- > " + ex.getMessage()); + } + } + + public String getAppSettings(String key) { + if (!key.isEmpty()) { + return appSettings.getProperty(key); + } + return ""; + } +} \ No newline at end of file diff --git a/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/Controllers/IncomingCallController.java b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/Controllers/IncomingCallController.java new file mode 100644 index 0000000..27b1024 --- /dev/null +++ b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/Controllers/IncomingCallController.java @@ -0,0 +1,96 @@ +package com.communication.MediaStreaming.Controllers; + +import com.communication.MediaStreaming.Logger; +import com.azure.core.util.BinaryData; +import com.azure.messaging.eventgrid.EventGridEvent; +import com.azure.messaging.eventgrid.SystemEventNames; +import com.azure.messaging.eventgrid.systemevents.SubscriptionValidationEventData; +import com.azure.messaging.eventgrid.systemevents.SubscriptionValidationResponse; +import com.communication.MediaStreaming.WebApp.CallConfiguration; +import com.communication.MediaStreaming.WebApp.MediaStreaming; +import com.communication.MediaStreaming.WebApp.EventHandler.EventAuthHandler; +import com.communication.MediaStreaming.WebApp.EventHandler.EventDispatcher; + +import java.util.List; +import com.google.gson.Gson; +import com.google.gson.JsonObject; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class IncomingCallController { + CallConfiguration callConfiguration; + + IncomingCallController(){ + callConfiguration = CallConfiguration.initiateConfiguration(); + } + + @PostMapping(value = "/OnIncomingCall", consumes = "application/json", produces = "application/json") + public ResponseEntity OnIncomingCall(@RequestBody(required = false) String data){ + List eventGridEvents = EventGridEvent.fromString(data); + + if(eventGridEvents.stream().count() > 0) + { + EventGridEvent eventGridEvent = eventGridEvents.get(0); + BinaryData eventData = eventGridEvent.getData(); + + if (eventGridEvent.getEventType().equals(SystemEventNames.EVENT_GRID_SUBSCRIPTION_VALIDATION)) + { + try { + SubscriptionValidationEventData subscriptionValidationEvent = eventData.toObject(SubscriptionValidationEventData.class); + SubscriptionValidationResponse responseData = new SubscriptionValidationResponse(); + responseData.setValidationResponse(subscriptionValidationEvent.getValidationCode()); + + return new ResponseEntity<>(responseData, HttpStatus.OK); + } catch (Exception e){ + e.printStackTrace(); + return new ResponseEntity<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); + } + } + else if(eventGridEvent.getEventType().equals("Microsoft.Communication.IncomingCall")){ + try { + JsonObject jsonData = new Gson().fromJson(eventGridEvent.getData().toString(), JsonObject.class); + if (data != null) { + + String callerId = jsonData.getAsJsonObject("from").get("rawId").getAsString(); + + if (data != null && (callerId == "*" + || callConfiguration.acceptCallsFrom.contains(callerId))) { + String incomingCallContext = jsonData.get("incomingCallContext").getAsString(); + Logger.logMessage(Logger.MessageType.INFORMATION, incomingCallContext); + new MediaStreaming(callConfiguration).report(incomingCallContext); + } + } + } + catch(Exception e){ + e.printStackTrace(); + return new ResponseEntity<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); + } + } + else{ + return new ResponseEntity<>(eventGridEvent.getEventType() + " is not handled.", HttpStatus.BAD_REQUEST); + } + } + return new ResponseEntity<>("Event count is not available.", HttpStatus.BAD_REQUEST); + } + + @RequestMapping("/api/MediaStreaming/callback") + public static String CallAutomationApiCallBack(@RequestBody(required = false) String data, + @RequestParam(value = "secret", required = false) String secretKey) { + EventAuthHandler eventhandler = EventAuthHandler.getInstance(); + + /// Validating the incoming request by using secret set in app.settings + if (eventhandler.authorize(secretKey)) { + (EventDispatcher.getInstance()).processNotification(data); + } else { + Logger.logMessage(Logger.MessageType.ERROR, "Unauthorized Request"); + } + return "OK"; + } +} diff --git a/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/EventHandler/EventAuthHandler.java b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/EventHandler/EventAuthHandler.java new file mode 100644 index 0000000..29eb769 --- /dev/null +++ b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/EventHandler/EventAuthHandler.java @@ -0,0 +1,34 @@ +package com.communication.MediaStreaming.EventHandler; + +import com.communication.MediaStreaming.ConfigurationManager; + +public class EventAuthHandler { + private String secretValue; + public static EventAuthHandler eventAuthHandler = null; + + public EventAuthHandler() { + ConfigurationManager configuration = ConfigurationManager.getInstance(); + secretValue = configuration.getAppSettings("SecretPlaceholder"); + + if (secretValue == null) { + System.out.println("SecretPlaceholder is null"); + secretValue = "h3llowW0rld"; + } + } + + public static EventAuthHandler getInstance() { + if (eventAuthHandler == null) { + eventAuthHandler = new EventAuthHandler(); + } + return eventAuthHandler; + } + + public Boolean authorize(String requestSecretValue) { + return requestSecretValue != null && requestSecretValue.equals(secretValue); + } + + public String getSecretQuerystring() { + String secretKey = "secret"; + return (secretKey + "=" + secretValue); + } +} \ No newline at end of file diff --git a/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/EventHandler/EventDispatcher.java b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/EventHandler/EventDispatcher.java new file mode 100644 index 0000000..0e78fdf --- /dev/null +++ b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/EventHandler/EventDispatcher.java @@ -0,0 +1,55 @@ +package com.communication.MediaStreaming.EventHandler; + +import com.azure.communication.callautomation.EventHandler; +import com.azure.communication.callautomation.models.events.CallAutomationEventBase; +import java.util.*; + +public class EventDispatcher { + private static EventDispatcher instance = null; + private final Hashtable notificationCallbacks; + + EventDispatcher() { + notificationCallbacks = new Hashtable<>(); + } + + /// + /// Get instances of EventDispatcher + /// + public static EventDispatcher getInstance() { + if (instance == null) { + instance = new EventDispatcher(); + } + return instance; + } + + public boolean subscribe(String eventType, String eventKey, NotificationCallback notificationCallback) { + String eventId = buildEventKey(eventType, eventKey); + synchronized (this) { + return (notificationCallbacks.put(eventId, notificationCallback) == null); + } + } + + public void unsubscribe(String eventType, String eventKey) { + String eventId = buildEventKey(eventType, eventKey); + synchronized (this) { + notificationCallbacks.remove(eventId); + } + } + + public String buildEventKey(String eventType, String eventKey) { + return (eventType + "-" + eventKey); + } + + public void processNotification(String request) { + CallAutomationEventBase callEvent = EventHandler.parseEvent(request); + if (callEvent != null) { + synchronized (this) { + final NotificationCallback notificationCallback = notificationCallbacks. + get(buildEventKey(callEvent.getClass().getName(), callEvent.getCallConnectionId())); + if (notificationCallback != null) { + new Thread(() -> notificationCallback.callback(callEvent)).start(); + } + } + } + } +} diff --git a/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/EventHandler/NotificationCallback.java b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/EventHandler/NotificationCallback.java new file mode 100644 index 0000000..3ee257d --- /dev/null +++ b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/EventHandler/NotificationCallback.java @@ -0,0 +1,7 @@ +package com.communication.MediaStreaming.EventHandler; + +import com.azure.communication.callautomation.models.events.CallAutomationEventBase; + +public interface NotificationCallback { + void callback(CallAutomationEventBase callEvent); +} \ No newline at end of file diff --git a/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/Logger.java b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/Logger.java new file mode 100644 index 0000000..cf2633d --- /dev/null +++ b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/Logger.java @@ -0,0 +1,22 @@ +package com.communication.MediaStreaming; + +public class Logger { + //Caution: Logging should be removed/disabled if you want to use this sample in production to avoid exposing sensitive information + public enum MessageType + { + INFORMATION, + ERROR + } + + /// + /// Log message to console + /// + /// Type of the message: Information or Error + /// Message string + public static void logMessage(MessageType messageType, String message) + { + String logMessage; + logMessage = messageType + " " + message; + System.out.println(logMessage); + } +} diff --git a/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/MediaStreaming.java b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/MediaStreaming.java new file mode 100644 index 0000000..b6b74b1 --- /dev/null +++ b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/MediaStreaming.java @@ -0,0 +1,93 @@ +package com.communication.MediaStreaming; + +import com.azure.communication.callautomation.CallAutomationClientBuilder; +import com.azure.communication.callautomation.CallAutomationClient; +import com.azure.communication.callautomation.models.AnswerCallOptions; +import com.azure.communication.callautomation.models.AnswerCallResult; +import com.azure.communication.callautomation.models.MediaStreamingAudioChannel; +import com.azure.communication.callautomation.models.MediaStreamingContent; +import com.azure.communication.callautomation.models.MediaStreamingOptions; +import com.azure.communication.callautomation.models.MediaStreamingTransport; +import com.azure.communication.callautomation.models.events.CallConnectedEvent; +import com.azure.communication.callautomation.models.events.CallDisconnectedEvent; + +import com.azure.cosmos.implementation.changefeed.CancellationTokenSource; +import com.communication.MediaStreaming.EventHandler.EventDispatcher; +import com.communication.MediaStreaming.EventHandler.NotificationCallback; +import com.azure.core.http.HttpHeader; +import com.azure.core.http.rest.Response; + +import java.util.concurrent.CompletableFuture; + +public class MediaStreaming { + + private final CallConfiguration callConfiguration; + private final CallAutomationClient callAutomationClient; + private CancellationTokenSource reportCancellationTokenSource; + private CompletableFuture callConnectedTask; + private CompletableFuture callTerminatedTask; + + public MediaStreaming(CallConfiguration callConfiguration) { + this.callConfiguration = callConfiguration; + this.callAutomationClient = new CallAutomationClientBuilder().connectionString(this.callConfiguration.connectionString) + .buildClient(); + } + + public void report(String incomingCallContext) { + reportCancellationTokenSource = new CancellationTokenSource(); + + try { + AnswerCallOptions answerCallOptions = new AnswerCallOptions(incomingCallContext, this.callConfiguration.appCallbackUrl); + + MediaStreamingOptions mediaStreamingOptions = new MediaStreamingOptions(this.callConfiguration.mediaStreamingTransportURI, + MediaStreamingTransport.WEBSOCKET, MediaStreamingContent.AUDIO, MediaStreamingAudioChannel.UNMIXED); + answerCallOptions.setMediaStreamingConfiguration(mediaStreamingOptions); + + Response response = this.callAutomationClient.answerCallWithResponse(answerCallOptions, null); + AnswerCallResult answerCallResult = response.getValue(); + + Logger.logMessage(Logger.MessageType.INFORMATION, "AnswerCallWithResponse -- > " + getResponse(response)); + + registerToCallStateChangeEvent(answerCallResult.getCallConnectionProperties().getCallConnectionId()); + //Wait for the call to get connected + callConnectedTask.get(); + // Wait for the call to terminate + callTerminatedTask.get(); + } catch (Exception ex) { + Logger.logMessage(Logger.MessageType.ERROR, "Call ended unexpectedly, reason -- > " + ex.getMessage()); + } + } + + private void registerToCallStateChangeEvent(String callLegId) { + callTerminatedTask = new CompletableFuture<>(); + callConnectedTask = new CompletableFuture<>(); + // Set the callback method + NotificationCallback callConnectedNotificaiton = ((callEvent) -> { + Logger.logMessage(Logger.MessageType.INFORMATION, "Call State successfully connected"); + callConnectedTask.complete(true); + EventDispatcher.getInstance().unsubscribe(CallConnectedEvent.class.getName(), callLegId); + }); + + NotificationCallback callDisconnectedNotificaiton = ((callEvent) -> { + EventDispatcher.getInstance().unsubscribe(CallDisconnectedEvent.class.getName(), callLegId); + reportCancellationTokenSource.cancel(); + callTerminatedTask.complete(true); + }); + + // Subscribe to the event + EventDispatcher.getInstance().subscribe(CallConnectedEvent.class.getName(), callLegId, callConnectedNotificaiton); + EventDispatcher.getInstance().subscribe(CallDisconnectedEvent.class.getName(), callLegId, callDisconnectedNotificaiton); + } + + public String getResponse(Response response) + { + StringBuilder responseString; + responseString = new StringBuilder("StatusCode: " + response.getStatusCode() + ", Headers: { "); + + for (HttpHeader header : response.getHeaders()) { + responseString.append(header.getName()).append(":").append(header.getValue()).append(", "); + } + responseString.append("} "); + return responseString.toString(); + } +} \ No newline at end of file diff --git a/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/config.properties b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/config.properties new file mode 100644 index 0000000..acfe690 --- /dev/null +++ b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/config.properties @@ -0,0 +1,13 @@ +# app settings +#Configurations related to Communication Service resource + +# Connection string of Azure Communication Service Resource. +Connectionstring=%Connectionstring% +# web pubsub URI +MediaStreamingTransportURI=%MediaStreamingTransportURI% +#url of the deployed API +AppCallBackUri=%AppCallBackUri% +# Secret for validating incoming request. +SecretPlaceholder=%SecretPlaceholder% +#Accept calls only from assigned participant/ or "*" for accepting all the calls +AcceptCallsFrom = * \ No newline at end of file diff --git a/CallAutomation_MediaStreaming/src/main/java/com/communication/WebSocketListener/App.java b/CallAutomation_MediaStreaming/src/main/java/com/communication/WebSocketListener/App.java new file mode 100644 index 0000000..b572b00 --- /dev/null +++ b/CallAutomation_MediaStreaming/src/main/java/com/communication/WebSocketListener/App.java @@ -0,0 +1,160 @@ +package com.communication.MediaStreaming.WebSocketListener; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.StringReader; +import java.net.ServerSocket; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import com.google.gson.Gson; +import com.google.gson.stream.JsonReader; + +public class App { + public static void main(String[] args) throws IOException, + NoSuchAlgorithmException { + ServerSocket server = new ServerSocket(8080); + Map audioDataFiles = null; + try { + System.out.println("Server has started on 127.0.0.1:80.\r\nWaiting for a connection…"); + while (true) { + Socket client = server.accept(); + System.out.println("A client connected."); + if (audioDataFiles == null) { + audioDataFiles = new HashMap(); + } + InputStream ins = client.getInputStream(); + OutputStream out = client.getOutputStream(); + byte[] receiveInput = new byte[2048]; + ins.read(receiveInput, 0, receiveInput.length); + String data = new String(receiveInput, StandardCharsets.UTF_8); + System.out.println(data); + Matcher get = Pattern.compile("^GET").matcher(data); + + if (get.find()) { + Matcher match = Pattern.compile("Sec-Websocket-Key: (.*)").matcher(data); + match.find(); + String socket_key = match.group(1).split(" ")[0]; + byte[] response = ("HTTP/1.1 101 Switching Protocols\r\n" + + "Connection: Upgrade\r\n" + + "Upgrade: websocket\r\n" + + "Sec-WebSocket-Accept: " + + Base64.getEncoder() + .encodeToString(MessageDigest.getInstance("SHA-1").digest( + (socket_key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").getBytes("UTF-8"))) + + "\r\n\r\n").getBytes("UTF-8"); + + out.write(response, 0, response.length); + + // Checking for the data streaming + while (client.isConnected() && !client.isClosed()) { + InputStream in = client.getInputStream(); + byte[] recvInput = new byte[client.getReceiveBufferSize()]; + in.read(recvInput); + if ((recvInput[0] & 127) == 1) { + String decodedData = DecodeData(recvInput); + try { + if (decodedData != null) { + Gson gson = new Gson(); + JsonReader reader = new JsonReader(new StringReader(decodedData)); + reader.setLenient(true); + AudioDataPackets jsonData = gson.fromJson(reader, AudioDataPackets.class); + + if (jsonData != null && jsonData.kind.equals("AudioData")) { + String dataAsString = jsonData.audioData.data; + byte[] byteArray = dataAsString.getBytes(); + + // generate file name and write data into file in dictionary + String fileName = String.format("%s.txt", jsonData.audioData.participantRawID) + .replace(":", ""); + FileOutputStream audioDataFileStream = null; + + if (audioDataFiles.containsKey(fileName)) { + audioDataFileStream = audioDataFiles.getOrDefault(fileName, null); + } else { + audioDataFileStream = new FileOutputStream(fileName); + audioDataFiles.put(fileName, audioDataFileStream); + } + audioDataFileStream.write(byteArray, 0, byteArray.length); + } + } + } catch (Exception ex) { + System.out.println("Exception ->" + ex); + } + } + + } + } + client.close(); + } + } catch (Exception ex) { + System.out.println(ex); + } finally { + for (Map.Entry entry : audioDataFiles.entrySet()) { + FileOutputStream value = entry.getValue(); + value.close(); + } + audioDataFiles.clear(); + } + server.close(); + } + + static String DecodeData(byte[] encodedData) { + byte secondByte = encodedData[1]; + int length = secondByte & (127); + int dataLength = 0; + int indexFirstMask = 2; + int extraBytes = 2; + + if (length == 126) { + extraBytes = 8; + indexFirstMask = 4; // if a special case, change indexFirstMask + } else if (length == 127) { + extraBytes = 14; + indexFirstMask = 10; + } else { + dataLength = length; + } + + for (int i = 2; i < indexFirstMask; i++) { + dataLength = (dataLength << 8) + (encodedData[i] & 0xFF); + } + + dataLength += extraBytes; + byte[] masks = new byte[4]; + + for (int i = 0; i < 4; i++) { + masks[i] = encodedData[indexFirstMask + i]; + } + + int indexFirstDataByte = indexFirstMask + 4; + byte[] decoded = new byte[dataLength]; + + for (int i = indexFirstDataByte, j = 0; i < dataLength; i++, j++) { + decoded[j] = (byte) (encodedData[i] ^ masks[j % 4]); + } + + String dataStream = new String(decoded, StandardCharsets.UTF_8); + return dataStream; + } + + public class AudioDataPackets { + public String kind; + public AudioData audioData; + } + + class AudioData { + public String data; // Base64 Encoded audio buffer data + public String timestamp; // In ISO 8601 format (yyyy-mm-ddThh:mm:ssZ) + public String participantRawID; + public boolean silent; // Indicates if the received audio buffer contains only silence. + } +} \ No newline at end of file From 57cf552ae0a907dccc1a25e915780a029b05d218 Mon Sep 17 00:00:00 2001 From: Mohammad Irfan Date: Tue, 29 Nov 2022 01:18:45 +0000 Subject: [PATCH 2/6] build issue fix --- .../Controllers/IncomingCallController.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/Controllers/IncomingCallController.java b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/Controllers/IncomingCallController.java index 27b1024..f4e826e 100644 --- a/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/Controllers/IncomingCallController.java +++ b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/Controllers/IncomingCallController.java @@ -6,10 +6,10 @@ import com.azure.messaging.eventgrid.SystemEventNames; import com.azure.messaging.eventgrid.systemevents.SubscriptionValidationEventData; import com.azure.messaging.eventgrid.systemevents.SubscriptionValidationResponse; -import com.communication.MediaStreaming.WebApp.CallConfiguration; -import com.communication.MediaStreaming.WebApp.MediaStreaming; -import com.communication.MediaStreaming.WebApp.EventHandler.EventAuthHandler; -import com.communication.MediaStreaming.WebApp.EventHandler.EventDispatcher; +import com.communication.MediaStreaming.CallConfiguration; +import com.communication.MediaStreaming.MediaStreaming; +import com.communication.MediaStreaming.EventHandler.EventAuthHandler; +import com.communication.MediaStreaming.EventHandler.EventDispatcher; import java.util.List; import com.google.gson.Gson; From f709e31223f16034d298b73fd24fd86aa3b77459 Mon Sep 17 00:00:00 2001 From: maulinasharma Date: Wed, 30 Nov 2022 16:38:55 +0530 Subject: [PATCH 3/6] Replace deserialization package from gson to fasterxml.jackson. --- .../communication/WebSocketListener/App.java | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/CallAutomation_MediaStreaming/src/main/java/com/communication/WebSocketListener/App.java b/CallAutomation_MediaStreaming/src/main/java/com/communication/WebSocketListener/App.java index b572b00..d7cbf15 100644 --- a/CallAutomation_MediaStreaming/src/main/java/com/communication/WebSocketListener/App.java +++ b/CallAutomation_MediaStreaming/src/main/java/com/communication/WebSocketListener/App.java @@ -1,10 +1,9 @@ -package com.communication.MediaStreaming.WebSocketListener; +package com.communication.WebSocketListener; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.io.StringReader; import java.net.ServerSocket; import java.net.Socket; import java.nio.charset.StandardCharsets; @@ -15,8 +14,7 @@ import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; -import com.google.gson.Gson; -import com.google.gson.stream.JsonReader; +import com.fasterxml.jackson.databind.ObjectMapper; public class App { public static void main(String[] args) throws IOException, @@ -63,14 +61,10 @@ public static void main(String[] args) throws IOException, String decodedData = DecodeData(recvInput); try { if (decodedData != null) { - Gson gson = new Gson(); - JsonReader reader = new JsonReader(new StringReader(decodedData)); - reader.setLenient(true); - AudioDataPackets jsonData = gson.fromJson(reader, AudioDataPackets.class); + AudioDataPackets jsonData = new ObjectMapper().readValue(decodedData, AudioDataPackets.class); if (jsonData != null && jsonData.kind.equals("AudioData")) { - String dataAsString = jsonData.audioData.data; - byte[] byteArray = dataAsString.getBytes(); + byte[] byteArray = jsonData.audioData.data; // generate file name and write data into file in dictionary String fileName = String.format("%s.txt", jsonData.audioData.participantRawID) @@ -146,13 +140,13 @@ static String DecodeData(byte[] encodedData) { return dataStream; } - public class AudioDataPackets { + public static class AudioDataPackets { public String kind; public AudioData audioData; } - class AudioData { - public String data; // Base64 Encoded audio buffer data + public static class AudioData { + public byte[] data; // Base64 Encoded audio buffer data public String timestamp; // In ISO 8601 format (yyyy-mm-ddThh:mm:ssZ) public String participantRawID; public boolean silent; // Indicates if the received audio buffer contains only silence. From 9b63093b51d689e2729b40ece64bb2b5c297a441 Mon Sep 17 00:00:00 2001 From: maulinasharma Date: Thu, 1 Dec 2022 18:15:44 +0530 Subject: [PATCH 4/6] Add log for callconnection id --- .../java/com/communication/MediaStreaming/MediaStreaming.java | 1 + 1 file changed, 1 insertion(+) diff --git a/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/MediaStreaming.java b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/MediaStreaming.java index b6b74b1..697e92f 100644 --- a/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/MediaStreaming.java +++ b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/MediaStreaming.java @@ -47,6 +47,7 @@ public void report(String incomingCallContext) { AnswerCallResult answerCallResult = response.getValue(); Logger.logMessage(Logger.MessageType.INFORMATION, "AnswerCallWithResponse -- > " + getResponse(response)); + Logger.logMessage(Logger.MessageType.INFORMATION, "Call Connection ID -- > " + answerCallResult.getCallConnectionProperties().getCallConnectionId()); registerToCallStateChangeEvent(answerCallResult.getCallConnectionProperties().getCallConnectionId()); //Wait for the call to get connected From 864e9185489d7403f64ca76addd57c81da4a83bd Mon Sep 17 00:00:00 2001 From: Netrapalli Sulochana Date: Wed, 21 Dec 2022 12:47:40 +0530 Subject: [PATCH 5/6] Formatted files --- .../com/communication/MediaStreaming/App.java | 9 +- .../MediaStreaming/CallConfiguration.java | 67 +++-- .../MediaStreaming/ConfigurationManager.java | 62 +++-- .../Controllers/IncomingCallController.java | 163 ++++++----- .../EventHandler/EventAuthHandler.java | 45 +-- .../EventHandler/EventDispatcher.java | 81 +++--- .../EventHandler/NotificationCallback.java | 4 +- .../communication/MediaStreaming/Logger.java | 33 ++- .../MediaStreaming/MediaStreaming.java | 196 ++++++++----- .../communication/WebSocketListener/App.java | 262 ++++++++++-------- 10 files changed, 537 insertions(+), 385 deletions(-) diff --git a/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/App.java b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/App.java index 7e3a237..9c32d1b 100644 --- a/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/App.java +++ b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/App.java @@ -2,13 +2,14 @@ // Licensed under the MIT License. package com.communication.MediaStreaming; + import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class App { - public static void main(String[] args) { - SpringApplication.run(App.class, args); - } -} \ No newline at end of file + public static void main(String[] args) { + SpringApplication.run(App.class, args); + } +} diff --git a/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/CallConfiguration.java b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/CallConfiguration.java index d7f3d9c..e6ed54f 100644 --- a/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/CallConfiguration.java +++ b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/CallConfiguration.java @@ -1,29 +1,50 @@ package com.communication.MediaStreaming; + import com.communication.MediaStreaming.EventHandler.EventAuthHandler; public class CallConfiguration { - public String connectionString; - public String appBaseUrl; - public String appCallbackUrl; - public String acceptCallsFrom; - public String mediaStreamingTransportURI; - public CallConfiguration(String connectionString, String appBaseUrl, String acceptCallsFrom, String mediaStreamingTransportURI) { - this.connectionString = connectionString; - this.appBaseUrl = appBaseUrl; - EventAuthHandler eventhandler = EventAuthHandler.getInstance(); - this.appCallbackUrl = appBaseUrl + "/api/IncomingCallMediaStreaming/callback?" + eventhandler.getSecretQuerystring(); - this.acceptCallsFrom = acceptCallsFrom; - this.mediaStreamingTransportURI = mediaStreamingTransportURI; - } + public String connectionString; + public String appBaseUrl; + public String appCallbackUrl; + public String acceptCallsFrom; + public String mediaStreamingTransportURI; + + public CallConfiguration( + String connectionString, + String appBaseUrl, + String acceptCallsFrom, + String mediaStreamingTransportURI + ) { + this.connectionString = connectionString; + this.appBaseUrl = appBaseUrl; + EventAuthHandler eventhandler = EventAuthHandler.getInstance(); + this.appCallbackUrl = + appBaseUrl + + "/api/IncomingCallMediaStreaming/callback?" + + eventhandler.getSecretQuerystring(); + this.acceptCallsFrom = acceptCallsFrom; + this.mediaStreamingTransportURI = mediaStreamingTransportURI; + } - public static CallConfiguration initiateConfiguration() { - ConfigurationManager configurationManager = ConfigurationManager.getInstance(); - configurationManager.loadAppSettings(); - String connectionString = configurationManager.getAppSettings("Connectionstring"); - String acceptCallsFrom = configurationManager.getAppSettings("AcceptCallsFrom"); - String mediaStreamingTransportURI = configurationManager.getAppSettings("MediaStreamingTransportURI"); - String appBaseUrl = configurationManager.getAppSettings("AppCallBackUri"); - return new CallConfiguration(connectionString, appBaseUrl, acceptCallsFrom, mediaStreamingTransportURI); - } -} \ No newline at end of file + public static CallConfiguration initiateConfiguration() { + ConfigurationManager configurationManager = ConfigurationManager.getInstance(); + configurationManager.loadAppSettings(); + String connectionString = configurationManager.getAppSettings( + "Connectionstring" + ); + String acceptCallsFrom = configurationManager.getAppSettings( + "AcceptCallsFrom" + ); + String mediaStreamingTransportURI = configurationManager.getAppSettings( + "MediaStreamingTransportURI" + ); + String appBaseUrl = configurationManager.getAppSettings("AppCallBackUri"); + return new CallConfiguration( + connectionString, + appBaseUrl, + acceptCallsFrom, + mediaStreamingTransportURI + ); + } +} diff --git a/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/ConfigurationManager.java b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/ConfigurationManager.java index ad93398..88e5493 100644 --- a/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/ConfigurationManager.java +++ b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/ConfigurationManager.java @@ -7,37 +7,45 @@ import java.util.Properties; public class ConfigurationManager { - private static ConfigurationManager configurationManager = null; - private final Properties appSettings = new Properties(); - private ConfigurationManager() { - } + private static ConfigurationManager configurationManager = null; + private final Properties appSettings = new Properties(); + + private ConfigurationManager() {} - // static method to create instance of ConfigurationManager class - public static ConfigurationManager getInstance() { - if (configurationManager == null) { - configurationManager = new ConfigurationManager(); - } - return configurationManager; + // static method to create instance of ConfigurationManager class + public static ConfigurationManager getInstance() { + if (configurationManager == null) { + configurationManager = new ConfigurationManager(); } + return configurationManager; + } - public void loadAppSettings() { - try { - File configFile = new File("src/main/java/com/communication/MediaStreaming/config.properties"); - FileReader reader = new FileReader(configFile); - appSettings.load(reader); - reader.close(); - } catch (FileNotFoundException ex) { - Logger.logMessage(Logger.MessageType.INFORMATION,"Loading app settings failed with error -- > " + ex.getMessage()); - } catch (IOException ex) { - Logger.logMessage(Logger.MessageType.ERROR,"Loading app settings failed with error -- > " + ex.getMessage()); - } + public void loadAppSettings() { + try { + File configFile = new File( + "src/main/java/com/communication/MediaStreaming/config.properties" + ); + FileReader reader = new FileReader(configFile); + appSettings.load(reader); + reader.close(); + } catch (FileNotFoundException ex) { + Logger.logMessage( + Logger.MessageType.INFORMATION, + "Loading app settings failed with error -- > " + ex.getMessage() + ); + } catch (IOException ex) { + Logger.logMessage( + Logger.MessageType.ERROR, + "Loading app settings failed with error -- > " + ex.getMessage() + ); } + } - public String getAppSettings(String key) { - if (!key.isEmpty()) { - return appSettings.getProperty(key); - } - return ""; + public String getAppSettings(String key) { + if (!key.isEmpty()) { + return appSettings.getProperty(key); } -} \ No newline at end of file + return ""; + } +} diff --git a/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/Controllers/IncomingCallController.java b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/Controllers/IncomingCallController.java index f4e826e..e68c54d 100644 --- a/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/Controllers/IncomingCallController.java +++ b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/Controllers/IncomingCallController.java @@ -1,20 +1,18 @@ package com.communication.MediaStreaming.Controllers; -import com.communication.MediaStreaming.Logger; import com.azure.core.util.BinaryData; import com.azure.messaging.eventgrid.EventGridEvent; import com.azure.messaging.eventgrid.SystemEventNames; import com.azure.messaging.eventgrid.systemevents.SubscriptionValidationEventData; import com.azure.messaging.eventgrid.systemevents.SubscriptionValidationResponse; import com.communication.MediaStreaming.CallConfiguration; -import com.communication.MediaStreaming.MediaStreaming; import com.communication.MediaStreaming.EventHandler.EventAuthHandler; import com.communication.MediaStreaming.EventHandler.EventDispatcher; - -import java.util.List; +import com.communication.MediaStreaming.Logger; +import com.communication.MediaStreaming.MediaStreaming; import com.google.gson.Gson; import com.google.gson.JsonObject; - +import java.util.List; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; @@ -25,72 +23,113 @@ @RestController public class IncomingCallController { - CallConfiguration callConfiguration; - IncomingCallController(){ - callConfiguration = CallConfiguration.initiateConfiguration(); - } + CallConfiguration callConfiguration; - @PostMapping(value = "/OnIncomingCall", consumes = "application/json", produces = "application/json") - public ResponseEntity OnIncomingCall(@RequestBody(required = false) String data){ - List eventGridEvents = EventGridEvent.fromString(data); + IncomingCallController() { + callConfiguration = CallConfiguration.initiateConfiguration(); + } - if(eventGridEvents.stream().count() > 0) - { - EventGridEvent eventGridEvent = eventGridEvents.get(0); - BinaryData eventData = eventGridEvent.getData(); + @PostMapping( + value = "/OnIncomingCall", + consumes = "application/json", + produces = "application/json" + ) + public ResponseEntity OnIncomingCall( + @RequestBody(required = false) String data + ) { + List eventGridEvents = EventGridEvent.fromString(data); - if (eventGridEvent.getEventType().equals(SystemEventNames.EVENT_GRID_SUBSCRIPTION_VALIDATION)) - { - try { - SubscriptionValidationEventData subscriptionValidationEvent = eventData.toObject(SubscriptionValidationEventData.class); - SubscriptionValidationResponse responseData = new SubscriptionValidationResponse(); - responseData.setValidationResponse(subscriptionValidationEvent.getValidationCode()); + if (eventGridEvents.stream().count() > 0) { + EventGridEvent eventGridEvent = eventGridEvents.get(0); + BinaryData eventData = eventGridEvent.getData(); - return new ResponseEntity<>(responseData, HttpStatus.OK); - } catch (Exception e){ - e.printStackTrace(); - return new ResponseEntity<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); - } - } - else if(eventGridEvent.getEventType().equals("Microsoft.Communication.IncomingCall")){ - try { - JsonObject jsonData = new Gson().fromJson(eventGridEvent.getData().toString(), JsonObject.class); - if (data != null) { + if ( + eventGridEvent + .getEventType() + .equals(SystemEventNames.EVENT_GRID_SUBSCRIPTION_VALIDATION) + ) { + try { + SubscriptionValidationEventData subscriptionValidationEvent = eventData.toObject( + SubscriptionValidationEventData.class + ); + SubscriptionValidationResponse responseData = new SubscriptionValidationResponse(); + responseData.setValidationResponse( + subscriptionValidationEvent.getValidationCode() + ); - String callerId = jsonData.getAsJsonObject("from").get("rawId").getAsString(); + return new ResponseEntity<>(responseData, HttpStatus.OK); + } catch (Exception e) { + e.printStackTrace(); + return new ResponseEntity<>( + e.getMessage(), + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } else if ( + eventGridEvent + .getEventType() + .equals("Microsoft.Communication.IncomingCall") + ) { + try { + JsonObject jsonData = new Gson() + .fromJson(eventGridEvent.getData().toString(), JsonObject.class); + if (data != null) { + String callerId = jsonData + .getAsJsonObject("from") + .get("rawId") + .getAsString(); - if (data != null && (callerId == "*" - || callConfiguration.acceptCallsFrom.contains(callerId))) { - String incomingCallContext = jsonData.get("incomingCallContext").getAsString(); - Logger.logMessage(Logger.MessageType.INFORMATION, incomingCallContext); - new MediaStreaming(callConfiguration).report(incomingCallContext); - } - } - } - catch(Exception e){ - e.printStackTrace(); - return new ResponseEntity<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); - } - } - else{ - return new ResponseEntity<>(eventGridEvent.getEventType() + " is not handled.", HttpStatus.BAD_REQUEST); + if ( + data != null && + ( + callerId == "*" || + callConfiguration.acceptCallsFrom.contains(callerId) + ) + ) { + String incomingCallContext = jsonData + .get("incomingCallContext") + .getAsString(); + Logger.logMessage( + Logger.MessageType.INFORMATION, + incomingCallContext + ); + new MediaStreaming(callConfiguration).report(incomingCallContext); } - } - return new ResponseEntity<>("Event count is not available.", HttpStatus.BAD_REQUEST); + } + } catch (Exception e) { + e.printStackTrace(); + return new ResponseEntity<>( + e.getMessage(), + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } else { + return new ResponseEntity<>( + eventGridEvent.getEventType() + " is not handled.", + HttpStatus.BAD_REQUEST + ); + } } + return new ResponseEntity<>( + "Event count is not available.", + HttpStatus.BAD_REQUEST + ); + } - @RequestMapping("/api/MediaStreaming/callback") - public static String CallAutomationApiCallBack(@RequestBody(required = false) String data, - @RequestParam(value = "secret", required = false) String secretKey) { - EventAuthHandler eventhandler = EventAuthHandler.getInstance(); + @RequestMapping("/api/MediaStreaming/callback") + public static String CallAutomationApiCallBack( + @RequestBody(required = false) String data, + @RequestParam(value = "secret", required = false) String secretKey + ) { + EventAuthHandler eventhandler = EventAuthHandler.getInstance(); - /// Validating the incoming request by using secret set in app.settings - if (eventhandler.authorize(secretKey)) { - (EventDispatcher.getInstance()).processNotification(data); - } else { - Logger.logMessage(Logger.MessageType.ERROR, "Unauthorized Request"); - } - return "OK"; - } + /// Validating the incoming request by using secret set in app.settings + if (eventhandler.authorize(secretKey)) { + (EventDispatcher.getInstance()).processNotification(data); + } else { + Logger.logMessage(Logger.MessageType.ERROR, "Unauthorized Request"); + } + return "OK"; + } } diff --git a/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/EventHandler/EventAuthHandler.java b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/EventHandler/EventAuthHandler.java index 29eb769..8ba5aa4 100644 --- a/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/EventHandler/EventAuthHandler.java +++ b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/EventHandler/EventAuthHandler.java @@ -3,32 +3,33 @@ import com.communication.MediaStreaming.ConfigurationManager; public class EventAuthHandler { - private String secretValue; - public static EventAuthHandler eventAuthHandler = null; - public EventAuthHandler() { - ConfigurationManager configuration = ConfigurationManager.getInstance(); - secretValue = configuration.getAppSettings("SecretPlaceholder"); + private String secretValue; + public static EventAuthHandler eventAuthHandler = null; - if (secretValue == null) { - System.out.println("SecretPlaceholder is null"); - secretValue = "h3llowW0rld"; - } - } + public EventAuthHandler() { + ConfigurationManager configuration = ConfigurationManager.getInstance(); + secretValue = configuration.getAppSettings("SecretPlaceholder"); - public static EventAuthHandler getInstance() { - if (eventAuthHandler == null) { - eventAuthHandler = new EventAuthHandler(); - } - return eventAuthHandler; + if (secretValue == null) { + System.out.println("SecretPlaceholder is null"); + secretValue = "h3llowW0rld"; } + } - public Boolean authorize(String requestSecretValue) { - return requestSecretValue != null && requestSecretValue.equals(secretValue); + public static EventAuthHandler getInstance() { + if (eventAuthHandler == null) { + eventAuthHandler = new EventAuthHandler(); } + return eventAuthHandler; + } - public String getSecretQuerystring() { - String secretKey = "secret"; - return (secretKey + "=" + secretValue); - } -} \ No newline at end of file + public Boolean authorize(String requestSecretValue) { + return requestSecretValue != null && requestSecretValue.equals(secretValue); + } + + public String getSecretQuerystring() { + String secretKey = "secret"; + return (secretKey + "=" + secretValue); + } +} diff --git a/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/EventHandler/EventDispatcher.java b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/EventHandler/EventDispatcher.java index 0e78fdf..d63b1f6 100644 --- a/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/EventHandler/EventDispatcher.java +++ b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/EventHandler/EventDispatcher.java @@ -5,51 +5,60 @@ import java.util.*; public class EventDispatcher { - private static EventDispatcher instance = null; - private final Hashtable notificationCallbacks; - EventDispatcher() { - notificationCallbacks = new Hashtable<>(); - } + private static EventDispatcher instance = null; + private final Hashtable notificationCallbacks; - /// - /// Get instances of EventDispatcher - /// - public static EventDispatcher getInstance() { - if (instance == null) { - instance = new EventDispatcher(); - } - return instance; - } + EventDispatcher() { + notificationCallbacks = new Hashtable<>(); + } - public boolean subscribe(String eventType, String eventKey, NotificationCallback notificationCallback) { - String eventId = buildEventKey(eventType, eventKey); - synchronized (this) { - return (notificationCallbacks.put(eventId, notificationCallback) == null); - } + /// + /// Get instances of EventDispatcher + /// + public static EventDispatcher getInstance() { + if (instance == null) { + instance = new EventDispatcher(); } + return instance; + } - public void unsubscribe(String eventType, String eventKey) { - String eventId = buildEventKey(eventType, eventKey); - synchronized (this) { - notificationCallbacks.remove(eventId); - } + public boolean subscribe( + String eventType, + String eventKey, + NotificationCallback notificationCallback + ) { + String eventId = buildEventKey(eventType, eventKey); + synchronized (this) { + return (notificationCallbacks.put(eventId, notificationCallback) == null); } + } - public String buildEventKey(String eventType, String eventKey) { - return (eventType + "-" + eventKey); + public void unsubscribe(String eventType, String eventKey) { + String eventId = buildEventKey(eventType, eventKey); + synchronized (this) { + notificationCallbacks.remove(eventId); } + } + + public String buildEventKey(String eventType, String eventKey) { + return (eventType + "-" + eventKey); + } - public void processNotification(String request) { - CallAutomationEventBase callEvent = EventHandler.parseEvent(request); - if (callEvent != null) { - synchronized (this) { - final NotificationCallback notificationCallback = notificationCallbacks. - get(buildEventKey(callEvent.getClass().getName(), callEvent.getCallConnectionId())); - if (notificationCallback != null) { - new Thread(() -> notificationCallback.callback(callEvent)).start(); - } - } + public void processNotification(String request) { + CallAutomationEventBase callEvent = EventHandler.parseEvent(request); + if (callEvent != null) { + synchronized (this) { + final NotificationCallback notificationCallback = notificationCallbacks.get( + buildEventKey( + callEvent.getClass().getName(), + callEvent.getCallConnectionId() + ) + ); + if (notificationCallback != null) { + new Thread(() -> notificationCallback.callback(callEvent)).start(); } + } } + } } diff --git a/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/EventHandler/NotificationCallback.java b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/EventHandler/NotificationCallback.java index 3ee257d..ffd5499 100644 --- a/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/EventHandler/NotificationCallback.java +++ b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/EventHandler/NotificationCallback.java @@ -3,5 +3,5 @@ import com.azure.communication.callautomation.models.events.CallAutomationEventBase; public interface NotificationCallback { - void callback(CallAutomationEventBase callEvent); -} \ No newline at end of file + void callback(CallAutomationEventBase callEvent); +} diff --git a/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/Logger.java b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/Logger.java index cf2633d..e228e16 100644 --- a/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/Logger.java +++ b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/Logger.java @@ -1,22 +1,21 @@ package com.communication.MediaStreaming; public class Logger { - //Caution: Logging should be removed/disabled if you want to use this sample in production to avoid exposing sensitive information - public enum MessageType - { - INFORMATION, - ERROR - } - /// - /// Log message to console - /// - /// Type of the message: Information or Error - /// Message string - public static void logMessage(MessageType messageType, String message) - { - String logMessage; - logMessage = messageType + " " + message; - System.out.println(logMessage); - } + //Caution: Logging should be removed/disabled if you want to use this sample in production to avoid exposing sensitive information + public enum MessageType { + INFORMATION, + ERROR, + } + + /// + /// Log message to console + /// + /// Type of the message: Information or Error + /// Message string + public static void logMessage(MessageType messageType, String message) { + String logMessage; + logMessage = messageType + " " + message; + System.out.println(logMessage); + } } diff --git a/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/MediaStreaming.java b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/MediaStreaming.java index 697e92f..28d711b 100644 --- a/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/MediaStreaming.java +++ b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/MediaStreaming.java @@ -1,7 +1,7 @@ package com.communication.MediaStreaming; -import com.azure.communication.callautomation.CallAutomationClientBuilder; import com.azure.communication.callautomation.CallAutomationClient; +import com.azure.communication.callautomation.CallAutomationClientBuilder; import com.azure.communication.callautomation.models.AnswerCallOptions; import com.azure.communication.callautomation.models.AnswerCallResult; import com.azure.communication.callautomation.models.MediaStreamingAudioChannel; @@ -10,85 +10,139 @@ import com.azure.communication.callautomation.models.MediaStreamingTransport; import com.azure.communication.callautomation.models.events.CallConnectedEvent; import com.azure.communication.callautomation.models.events.CallDisconnectedEvent; - +import com.azure.core.http.HttpHeader; +import com.azure.core.http.rest.Response; import com.azure.cosmos.implementation.changefeed.CancellationTokenSource; import com.communication.MediaStreaming.EventHandler.EventDispatcher; import com.communication.MediaStreaming.EventHandler.NotificationCallback; -import com.azure.core.http.HttpHeader; -import com.azure.core.http.rest.Response; - import java.util.concurrent.CompletableFuture; public class MediaStreaming { - private final CallConfiguration callConfiguration; - private final CallAutomationClient callAutomationClient; - private CancellationTokenSource reportCancellationTokenSource; - private CompletableFuture callConnectedTask; - private CompletableFuture callTerminatedTask; - - public MediaStreaming(CallConfiguration callConfiguration) { - this.callConfiguration = callConfiguration; - this.callAutomationClient = new CallAutomationClientBuilder().connectionString(this.callConfiguration.connectionString) + private final CallConfiguration callConfiguration; + private final CallAutomationClient callAutomationClient; + private CancellationTokenSource reportCancellationTokenSource; + private CompletableFuture callConnectedTask; + private CompletableFuture callTerminatedTask; + + public MediaStreaming(CallConfiguration callConfiguration) { + this.callConfiguration = callConfiguration; + this.callAutomationClient = + new CallAutomationClientBuilder() + .connectionString(this.callConfiguration.connectionString) .buildClient(); + } + + public void report(String incomingCallContext) { + reportCancellationTokenSource = new CancellationTokenSource(); + + try { + AnswerCallOptions answerCallOptions = new AnswerCallOptions( + incomingCallContext, + this.callConfiguration.appCallbackUrl + ); + + MediaStreamingOptions mediaStreamingOptions = new MediaStreamingOptions( + this.callConfiguration.mediaStreamingTransportURI, + MediaStreamingTransport.WEBSOCKET, + MediaStreamingContent.AUDIO, + MediaStreamingAudioChannel.UNMIXED + ); + answerCallOptions.setMediaStreamingConfiguration(mediaStreamingOptions); + + Response response = + this.callAutomationClient.answerCallWithResponse( + answerCallOptions, + null + ); + AnswerCallResult answerCallResult = response.getValue(); + + Logger.logMessage( + Logger.MessageType.INFORMATION, + "AnswerCallWithResponse -- > " + getResponse(response) + ); + Logger.logMessage( + Logger.MessageType.INFORMATION, + "Call Connection ID -- > " + + answerCallResult.getCallConnectionProperties().getCallConnectionId() + ); + + registerToCallStateChangeEvent( + answerCallResult.getCallConnectionProperties().getCallConnectionId() + ); + //Wait for the call to get connected + callConnectedTask.get(); + // Wait for the call to terminate + callTerminatedTask.get(); + } catch (Exception ex) { + Logger.logMessage( + Logger.MessageType.ERROR, + "Call ended unexpectedly, reason -- > " + ex.getMessage() + ); } - - public void report(String incomingCallContext) { - reportCancellationTokenSource = new CancellationTokenSource(); - - try { - AnswerCallOptions answerCallOptions = new AnswerCallOptions(incomingCallContext, this.callConfiguration.appCallbackUrl); - - MediaStreamingOptions mediaStreamingOptions = new MediaStreamingOptions(this.callConfiguration.mediaStreamingTransportURI, - MediaStreamingTransport.WEBSOCKET, MediaStreamingContent.AUDIO, MediaStreamingAudioChannel.UNMIXED); - answerCallOptions.setMediaStreamingConfiguration(mediaStreamingOptions); - - Response response = this.callAutomationClient.answerCallWithResponse(answerCallOptions, null); - AnswerCallResult answerCallResult = response.getValue(); - - Logger.logMessage(Logger.MessageType.INFORMATION, "AnswerCallWithResponse -- > " + getResponse(response)); - Logger.logMessage(Logger.MessageType.INFORMATION, "Call Connection ID -- > " + answerCallResult.getCallConnectionProperties().getCallConnectionId()); - - registerToCallStateChangeEvent(answerCallResult.getCallConnectionProperties().getCallConnectionId()); - //Wait for the call to get connected - callConnectedTask.get(); - // Wait for the call to terminate - callTerminatedTask.get(); - } catch (Exception ex) { - Logger.logMessage(Logger.MessageType.ERROR, "Call ended unexpectedly, reason -- > " + ex.getMessage()); + } + + private void registerToCallStateChangeEvent(String callLegId) { + callTerminatedTask = new CompletableFuture<>(); + callConnectedTask = new CompletableFuture<>(); + // Set the callback method + NotificationCallback callConnectedNotificaiton = + ( + callEvent -> { + Logger.logMessage( + Logger.MessageType.INFORMATION, + "Call State successfully connected" + ); + callConnectedTask.complete(true); + EventDispatcher + .getInstance() + .unsubscribe(CallConnectedEvent.class.getName(), callLegId); } - } - - private void registerToCallStateChangeEvent(String callLegId) { - callTerminatedTask = new CompletableFuture<>(); - callConnectedTask = new CompletableFuture<>(); - // Set the callback method - NotificationCallback callConnectedNotificaiton = ((callEvent) -> { - Logger.logMessage(Logger.MessageType.INFORMATION, "Call State successfully connected"); - callConnectedTask.complete(true); - EventDispatcher.getInstance().unsubscribe(CallConnectedEvent.class.getName(), callLegId); - }); - - NotificationCallback callDisconnectedNotificaiton = ((callEvent) -> { - EventDispatcher.getInstance().unsubscribe(CallDisconnectedEvent.class.getName(), callLegId); - reportCancellationTokenSource.cancel(); - callTerminatedTask.complete(true); - }); - - // Subscribe to the event - EventDispatcher.getInstance().subscribe(CallConnectedEvent.class.getName(), callLegId, callConnectedNotificaiton); - EventDispatcher.getInstance().subscribe(CallDisconnectedEvent.class.getName(), callLegId, callDisconnectedNotificaiton); - } - - public String getResponse(Response response) - { - StringBuilder responseString; - responseString = new StringBuilder("StatusCode: " + response.getStatusCode() + ", Headers: { "); - - for (HttpHeader header : response.getHeaders()) { - responseString.append(header.getName()).append(":").append(header.getValue()).append(", "); + ); + + NotificationCallback callDisconnectedNotificaiton = + ( + callEvent -> { + EventDispatcher + .getInstance() + .unsubscribe(CallDisconnectedEvent.class.getName(), callLegId); + reportCancellationTokenSource.cancel(); + callTerminatedTask.complete(true); } - responseString.append("} "); - return responseString.toString(); + ); + + // Subscribe to the event + EventDispatcher + .getInstance() + .subscribe( + CallConnectedEvent.class.getName(), + callLegId, + callConnectedNotificaiton + ); + EventDispatcher + .getInstance() + .subscribe( + CallDisconnectedEvent.class.getName(), + callLegId, + callDisconnectedNotificaiton + ); + } + + public String getResponse(Response response) { + StringBuilder responseString; + responseString = + new StringBuilder( + "StatusCode: " + response.getStatusCode() + ", Headers: { " + ); + + for (HttpHeader header : response.getHeaders()) { + responseString + .append(header.getName()) + .append(":") + .append(header.getValue()) + .append(", "); } -} \ No newline at end of file + responseString.append("} "); + return responseString.toString(); + } +} diff --git a/CallAutomation_MediaStreaming/src/main/java/com/communication/WebSocketListener/App.java b/CallAutomation_MediaStreaming/src/main/java/com/communication/WebSocketListener/App.java index d7cbf15..84b94e1 100644 --- a/CallAutomation_MediaStreaming/src/main/java/com/communication/WebSocketListener/App.java +++ b/CallAutomation_MediaStreaming/src/main/java/com/communication/WebSocketListener/App.java @@ -1,5 +1,6 @@ package com.communication.WebSocketListener; +import com.fasterxml.jackson.databind.ObjectMapper; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; @@ -14,141 +15,160 @@ import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; -import com.fasterxml.jackson.databind.ObjectMapper; public class App { - public static void main(String[] args) throws IOException, - NoSuchAlgorithmException { - ServerSocket server = new ServerSocket(8080); - Map audioDataFiles = null; - try { - System.out.println("Server has started on 127.0.0.1:80.\r\nWaiting for a connection…"); - while (true) { - Socket client = server.accept(); - System.out.println("A client connected."); - if (audioDataFiles == null) { - audioDataFiles = new HashMap(); - } - InputStream ins = client.getInputStream(); - OutputStream out = client.getOutputStream(); - byte[] receiveInput = new byte[2048]; - ins.read(receiveInput, 0, receiveInput.length); - String data = new String(receiveInput, StandardCharsets.UTF_8); - System.out.println(data); - Matcher get = Pattern.compile("^GET").matcher(data); - - if (get.find()) { - Matcher match = Pattern.compile("Sec-Websocket-Key: (.*)").matcher(data); - match.find(); - String socket_key = match.group(1).split(" ")[0]; - byte[] response = ("HTTP/1.1 101 Switching Protocols\r\n" - + "Connection: Upgrade\r\n" - + "Upgrade: websocket\r\n" - + "Sec-WebSocket-Accept: " - + Base64.getEncoder() - .encodeToString(MessageDigest.getInstance("SHA-1").digest( - (socket_key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").getBytes("UTF-8"))) - + "\r\n\r\n").getBytes("UTF-8"); - - out.write(response, 0, response.length); - - // Checking for the data streaming - while (client.isConnected() && !client.isClosed()) { - InputStream in = client.getInputStream(); - byte[] recvInput = new byte[client.getReceiveBufferSize()]; - in.read(recvInput); - if ((recvInput[0] & 127) == 1) { - String decodedData = DecodeData(recvInput); - try { - if (decodedData != null) { - AudioDataPackets jsonData = new ObjectMapper().readValue(decodedData, AudioDataPackets.class); - - if (jsonData != null && jsonData.kind.equals("AudioData")) { - byte[] byteArray = jsonData.audioData.data; - - // generate file name and write data into file in dictionary - String fileName = String.format("%s.txt", jsonData.audioData.participantRawID) - .replace(":", ""); - FileOutputStream audioDataFileStream = null; - - if (audioDataFiles.containsKey(fileName)) { - audioDataFileStream = audioDataFiles.getOrDefault(fileName, null); - } else { - audioDataFileStream = new FileOutputStream(fileName); - audioDataFiles.put(fileName, audioDataFileStream); - } - audioDataFileStream.write(byteArray, 0, byteArray.length); - } - } - } catch (Exception ex) { - System.out.println("Exception ->" + ex); - } - } + public static void main(String[] args) + throws IOException, NoSuchAlgorithmException { + ServerSocket server = new ServerSocket(8080); + Map audioDataFiles = null; + try { + System.out.println( + "Server has started on 127.0.0.1:80.\r\nWaiting for a connection…" + ); + while (true) { + Socket client = server.accept(); + System.out.println("A client connected."); + if (audioDataFiles == null) { + audioDataFiles = new HashMap(); + } + InputStream ins = client.getInputStream(); + OutputStream out = client.getOutputStream(); + byte[] receiveInput = new byte[2048]; + ins.read(receiveInput, 0, receiveInput.length); + String data = new String(receiveInput, StandardCharsets.UTF_8); + System.out.println(data); + Matcher get = Pattern.compile("^GET").matcher(data); + + if (get.find()) { + Matcher match = Pattern + .compile("Sec-Websocket-Key: (.*)") + .matcher(data); + match.find(); + String socket_key = match.group(1).split(" ")[0]; + byte[] response = + ( + "HTTP/1.1 101 Switching Protocols\r\n" + + "Connection: Upgrade\r\n" + + "Upgrade: websocket\r\n" + + "Sec-WebSocket-Accept: " + + Base64 + .getEncoder() + .encodeToString( + MessageDigest + .getInstance("SHA-1") + .digest( + ( + socket_key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + ).getBytes("UTF-8") + ) + ) + + "\r\n\r\n" + ).getBytes("UTF-8"); + + out.write(response, 0, response.length); + + // Checking for the data streaming + while (client.isConnected() && !client.isClosed()) { + InputStream in = client.getInputStream(); + byte[] recvInput = new byte[client.getReceiveBufferSize()]; + in.read(recvInput); + if ((recvInput[0] & 127) == 1) { + String decodedData = DecodeData(recvInput); + try { + if (decodedData != null) { + AudioDataPackets jsonData = new ObjectMapper() + .readValue(decodedData, AudioDataPackets.class); + + if (jsonData != null && jsonData.kind.equals("AudioData")) { + byte[] byteArray = jsonData.audioData.data; + + // generate file name and write data into file in dictionary + String fileName = String + .format("%s.txt", jsonData.audioData.participantRawID) + .replace(":", ""); + FileOutputStream audioDataFileStream = null; + + if (audioDataFiles.containsKey(fileName)) { + audioDataFileStream = + audioDataFiles.getOrDefault(fileName, null); + } else { + audioDataFileStream = new FileOutputStream(fileName); + audioDataFiles.put(fileName, audioDataFileStream); } + audioDataFileStream.write(byteArray, 0, byteArray.length); + } } - client.close(); - } - } catch (Exception ex) { - System.out.println(ex); - } finally { - for (Map.Entry entry : audioDataFiles.entrySet()) { - FileOutputStream value = entry.getValue(); - value.close(); + } catch (Exception ex) { + System.out.println("Exception ->" + ex); + } } - audioDataFiles.clear(); + } } - server.close(); + client.close(); + } + } catch (Exception ex) { + System.out.println(ex); + } finally { + for (Map.Entry entry : audioDataFiles.entrySet()) { + FileOutputStream value = entry.getValue(); + value.close(); + } + audioDataFiles.clear(); + } + server.close(); + } + + static String DecodeData(byte[] encodedData) { + byte secondByte = encodedData[1]; + int length = secondByte & (127); + int dataLength = 0; + int indexFirstMask = 2; + int extraBytes = 2; + + if (length == 126) { + extraBytes = 8; + indexFirstMask = 4; // if a special case, change indexFirstMask + } else if (length == 127) { + extraBytes = 14; + indexFirstMask = 10; + } else { + dataLength = length; } - static String DecodeData(byte[] encodedData) { - byte secondByte = encodedData[1]; - int length = secondByte & (127); - int dataLength = 0; - int indexFirstMask = 2; - int extraBytes = 2; - - if (length == 126) { - extraBytes = 8; - indexFirstMask = 4; // if a special case, change indexFirstMask - } else if (length == 127) { - extraBytes = 14; - indexFirstMask = 10; - } else { - dataLength = length; - } + for (int i = 2; i < indexFirstMask; i++) { + dataLength = (dataLength << 8) + (encodedData[i] & 0xFF); + } - for (int i = 2; i < indexFirstMask; i++) { - dataLength = (dataLength << 8) + (encodedData[i] & 0xFF); - } + dataLength += extraBytes; + byte[] masks = new byte[4]; - dataLength += extraBytes; - byte[] masks = new byte[4]; + for (int i = 0; i < 4; i++) { + masks[i] = encodedData[indexFirstMask + i]; + } - for (int i = 0; i < 4; i++) { - masks[i] = encodedData[indexFirstMask + i]; - } + int indexFirstDataByte = indexFirstMask + 4; + byte[] decoded = new byte[dataLength]; - int indexFirstDataByte = indexFirstMask + 4; - byte[] decoded = new byte[dataLength]; + for (int i = indexFirstDataByte, j = 0; i < dataLength; i++, j++) { + decoded[j] = (byte) (encodedData[i] ^ masks[j % 4]); + } - for (int i = indexFirstDataByte, j = 0; i < dataLength; i++, j++) { - decoded[j] = (byte) (encodedData[i] ^ masks[j % 4]); - } + String dataStream = new String(decoded, StandardCharsets.UTF_8); + return dataStream; + } - String dataStream = new String(decoded, StandardCharsets.UTF_8); - return dataStream; - } + public static class AudioDataPackets { - public static class AudioDataPackets { - public String kind; - public AudioData audioData; - } + public String kind; + public AudioData audioData; + } - public static class AudioData { - public byte[] data; // Base64 Encoded audio buffer data - public String timestamp; // In ISO 8601 format (yyyy-mm-ddThh:mm:ssZ) - public String participantRawID; - public boolean silent; // Indicates if the received audio buffer contains only silence. - } -} \ No newline at end of file + public static class AudioData { + + public byte[] data; // Base64 Encoded audio buffer data + public String timestamp; // In ISO 8601 format (yyyy-mm-ddThh:mm:ssZ) + public String participantRawID; + public boolean silent; // Indicates if the received audio buffer contains only silence. + } +} From 313638d020d1947556054526df63536e582383aa Mon Sep 17 00:00:00 2001 From: Netrapalli Sulochana Date: Thu, 22 Dec 2022 13:57:47 +0530 Subject: [PATCH 6/6] Updated If block with SystemEventNames.EVENT_GRID_SUBSCRIPTION_VALIDATION.equals(eventGridEvent.getEventType()) && eventGridEvent!=null --- .../MediaStreaming/Controllers/IncomingCallController.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/Controllers/IncomingCallController.java b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/Controllers/IncomingCallController.java index e68c54d..e380cb4 100644 --- a/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/Controllers/IncomingCallController.java +++ b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/Controllers/IncomingCallController.java @@ -45,9 +45,7 @@ public ResponseEntity OnIncomingCall( BinaryData eventData = eventGridEvent.getData(); if ( - eventGridEvent - .getEventType() - .equals(SystemEventNames.EVENT_GRID_SUBSCRIPTION_VALIDATION) + SystemEventNames.EVENT_GRID_SUBSCRIPTION_VALIDATION.equals(eventGridEvent.getEventType()) && eventGridEvent!=null ) { try { SubscriptionValidationEventData subscriptionValidationEvent = eventData.toObject(