diff --git a/CallAutomation_MediaStreaming/media/EventgridSubscription-IncomingCall.png b/CallAutomation_MediaStreaming/media/EventgridSubscription-IncomingCall.png new file mode 100644 index 0000000..7983b61 Binary files /dev/null and b/CallAutomation_MediaStreaming/media/EventgridSubscription-IncomingCall.png differ diff --git a/CallAutomation_MediaStreaming/pom.xml b/CallAutomation_MediaStreaming/pom.xml new file mode 100644 index 0000000..f70cec4 --- /dev/null +++ b/CallAutomation_MediaStreaming/pom.xml @@ -0,0 +1,157 @@ + + + + 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..9c32d1b --- /dev/null +++ b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/App.java @@ -0,0 +1,15 @@ +// 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); + } +} 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..e6ed54f --- /dev/null +++ b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/CallConfiguration.java @@ -0,0 +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 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 new file mode 100644 index 0000000..88e5493 --- /dev/null +++ b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/ConfigurationManager.java @@ -0,0 +1,51 @@ +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 ""; + } +} 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..e380cb4 --- /dev/null +++ b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/Controllers/IncomingCallController.java @@ -0,0 +1,133 @@ +package com.communication.MediaStreaming.Controllers; + +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.EventHandler.EventAuthHandler; +import com.communication.MediaStreaming.EventHandler.EventDispatcher; +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; +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 ( + SystemEventNames.EVENT_GRID_SUBSCRIPTION_VALIDATION.equals(eventGridEvent.getEventType()) && eventGridEvent!=null + ) { + 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..8ba5aa4 --- /dev/null +++ b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/EventHandler/EventAuthHandler.java @@ -0,0 +1,35 @@ +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); + } +} 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..d63b1f6 --- /dev/null +++ b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/EventHandler/EventDispatcher.java @@ -0,0 +1,64 @@ +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..ffd5499 --- /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); +} 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..e228e16 --- /dev/null +++ b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/Logger.java @@ -0,0 +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); + } +} 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..28d711b --- /dev/null +++ b/CallAutomation_MediaStreaming/src/main/java/com/communication/MediaStreaming/MediaStreaming.java @@ -0,0 +1,148 @@ +package com.communication.MediaStreaming; + +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; +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.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 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) + ); + 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); + } + ); + + 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(); + } +} 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..84b94e1 --- /dev/null +++ b/CallAutomation_MediaStreaming/src/main/java/com/communication/WebSocketListener/App.java @@ -0,0 +1,174 @@ +package com.communication.WebSocketListener; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +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; + +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); + } + } + } + } + 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 static class AudioDataPackets { + + 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. + } +}