diff --git a/LobbyCallSupportSample/LobbyCallSupportSample.sln b/LobbyCallSupportSample/LobbyCallSupportSample.sln new file mode 100644 index 00000000..3a659249 --- /dev/null +++ b/LobbyCallSupportSample/LobbyCallSupportSample.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.12.35728.132 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LobbyCallSupportSample", "LobbyCallSupportSample\LobbyCallSupportSample.csproj", "{3CAA0D48-2795-C343-DA4B-6B3F56CBB314}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3CAA0D48-2795-C343-DA4B-6B3F56CBB314}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3CAA0D48-2795-C343-DA4B-6B3F56CBB314}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3CAA0D48-2795-C343-DA4B-6B3F56CBB314}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3CAA0D48-2795-C343-DA4B-6B3F56CBB314}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {99998281-4DF2-4536-B2AD-266FB84E2025} + EndGlobalSection +EndGlobal diff --git a/LobbyCallSupportSample/LobbyCallSupportSample/Helper.cs b/LobbyCallSupportSample/LobbyCallSupportSample/Helper.cs new file mode 100644 index 00000000..99f1a5bc --- /dev/null +++ b/LobbyCallSupportSample/LobbyCallSupportSample/Helper.cs @@ -0,0 +1,100 @@ +using Azure.Communication.CallAutomation; +using System.Net.WebSockets; +using System.Text; +using System.Text.Json; + +namespace LobbyCallSupportSample +{ + public static class Helper + { + public static async Task ProcessRequest(WebSocket webSocket) + { + try + { + var buffer = new byte[1024 * 4]; + var cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(60)).Token; + WebSocketReceiveResult receiveResult = await webSocket.ReceiveAsync(new ArraySegment(buffer), cancellationToken); + + while (!receiveResult.CloseStatus.HasValue) + { + string msg = Encoding.UTF8.GetString(buffer, 0, receiveResult.Count); + + var response = StreamingData.Parse(msg); + + if (response != null) + { + if (response is AudioMetadata audioMetadata) + { + Console.WriteLine("***************************************************************************************"); + Console.WriteLine("MEDIA SUBSCRIPTION ID-->" + audioMetadata.MediaSubscriptionId); + Console.WriteLine("ENCODING-->" + audioMetadata.Encoding); + Console.WriteLine("SAMPLE RATE-->" + audioMetadata.SampleRate); + Console.WriteLine("CHANNELS-->" + audioMetadata.Channels); + //Console.WriteLine("LENGTH-->" + audioMetadata.Length); + Console.WriteLine("***************************************************************************************"); + } + if (response is AudioData audioData) + { + Console.WriteLine("***************************************************************************************"); + Console.WriteLine("DATA-->" + JsonSerializer.Serialize(audioData.Data)); + Console.WriteLine("TIMESTAMP-->" + audioData.Timestamp); + Console.WriteLine("IS SILENT-->" + audioData.IsSilent); + if (audioData.Participant != null && audioData.Participant.RawId != null) + { + Console.WriteLine("Participant Id-->" + audioData.Participant.RawId); + } + + Console.WriteLine("***************************************************************************************"); + } + + if (response is TranscriptionMetadata transcriptionMetadata) + { + Console.WriteLine("***************************************************************************************"); + Console.WriteLine("TRANSCRIPTION SUBSCRIPTION ID-->" + transcriptionMetadata.TranscriptionSubscriptionId); + Console.WriteLine("LOCALE-->" + transcriptionMetadata.Locale); + Console.WriteLine("CALL CONNECTION ID--?" + transcriptionMetadata.CallConnectionId); + Console.WriteLine("CORRELATION ID-->" + transcriptionMetadata.CorrelationId); + Console.WriteLine("***************************************************************************************"); + } + if (response is TranscriptionData transcriptionData) + { + Console.WriteLine("***************************************************************************************"); + Console.WriteLine("TEXT-->" + transcriptionData.Text); + Console.WriteLine("FORMAT-->" + transcriptionData.Format); + Console.WriteLine("OFFSET-->" + transcriptionData.Offset); + Console.WriteLine("DURATION-->" + transcriptionData.Duration); + Console.WriteLine("PARTICIPANT-->" + transcriptionData.Participant.RawId); + Console.WriteLine("CONFIDENCE-->" + transcriptionData.Confidence); + Console.WriteLine("RESULT STATUS-->" + transcriptionData.ResultState); + foreach (var word in transcriptionData.Words) + { + Console.WriteLine("WORDS TEXT-->" + word.Text); + Console.WriteLine("WORDS OFFSET-->" + word.Offset); + Console.WriteLine("WORDS DURATION-->" + word.Duration); + } + Console.WriteLine("***************************************************************************************"); + } + } + + await webSocket.SendAsync( + new ArraySegment(buffer, 0, receiveResult.Count), + receiveResult.MessageType, + receiveResult.EndOfMessage, + CancellationToken.None); + + receiveResult = await webSocket.ReceiveAsync( + new ArraySegment(buffer), CancellationToken.None); + } + + await webSocket.CloseAsync(receiveResult.CloseStatus.Value, receiveResult.CloseStatusDescription, CancellationToken.None); + } + catch (Exception ex) + { + Console.WriteLine($"Exception -> {ex}"); + } + finally + { + } + } + } +} diff --git a/LobbyCallSupportSample/LobbyCallSupportSample/LobbyCallSupportSample.csproj b/LobbyCallSupportSample/LobbyCallSupportSample/LobbyCallSupportSample.csproj new file mode 100644 index 00000000..88be9e65 --- /dev/null +++ b/LobbyCallSupportSample/LobbyCallSupportSample/LobbyCallSupportSample.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + diff --git a/LobbyCallSupportSample/LobbyCallSupportSample/Program.cs b/LobbyCallSupportSample/LobbyCallSupportSample/Program.cs new file mode 100644 index 00000000..af77df5a --- /dev/null +++ b/LobbyCallSupportSample/LobbyCallSupportSample/Program.cs @@ -0,0 +1,531 @@ +using Azure.Communication; +using Azure.Communication.CallAutomation; +using Azure.Core; +using Azure.Messaging; +using Azure.Messaging.EventGrid; +using Azure.Messaging.EventGrid.SystemEvents; +using Microsoft.Extensions.Logging; +using System.Net.WebSockets; +using System.Text; + +#region Bootstrap +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); +#endregion + +#region Global Variables for LobbyCallSupportSample + +string + // Configuration variables + acsConnectionString = + builder.Configuration["acsConnectionString"] + ?? throw new ArgumentNullException("acsConnectionString"), + cognitiveServiceEndpoint = + builder.Configuration["cognitiveServiceEndpoint"] + ?? throw new ArgumentNullException("cognitiveServiceEndpoint"), + callbackUriHost = + builder.Configuration["callbackUriHost"] + ?? throw new ArgumentNullException("callbackUriHost"), + acsGeneratedIdForLobbyCallReceiver = + builder.Configuration["acsGeneratedIdForLobbyCallReceiver"] + ?? throw new ArgumentNullException("acsGeneratedIdForLobbyCallReceiver"), + acsGeneratedIdForTargetCallReceiver = + builder.Configuration["acsGeneratedIdForTargetCallReceiver"] + ?? throw new ArgumentNullException("acsGeneratedIdForTargetCallReceiver"), + acsGeneratedIdForTargetCallSender = + builder.Configuration["acsGeneratedIdForTargetCallSender"] + ?? throw new ArgumentNullException("acsGeneratedIdForTargetCallSender"), + confirmMessageToTargetCall = "A user is waiting in lobby, do you want to add the lobby user to your call?", + textToPlayToLobbyUser = "You are currently in a lobby call, we will notify the admin that you are waiting.", + // Track which type of workflow call was last created + lastWorkflowCallType = string.Empty, // "CallTwo" or "CallThree" + acsIdentity = string.Empty, + // Call connection IDs + targetCallConnectionId = string.Empty, + lobbyConnectionId = string.Empty, // User's incoming call connection id + lobbyCallerId = string.Empty, // User's incoming caller id + callConnectionId2 = string.Empty; // ACS user's redirected call + +// Web socket +WebSocket? webSocket = null; + +CallAutomationClient client = + new(connectionString: acsConnectionString); +#endregion + +#region Event Handler + +app.MapPost("/api/LobbyCallSupportEventHandler", async (EventGridEvent[] eventGridEvents, ILogger logger) => +{ + StringBuilder msgLog = new(); // to make string builder thread-safe; declared here + msgLog.AppendLine(""" + + ~~~~~~~~~~~~ /api/LobbyCallSupportEventHandler ~~~~~~~~~~~~ + """); + try + { + + foreach (var eventGridEvent in eventGridEvents) + { + if (eventGridEvent.TryGetSystemEventData(out object eventData)) + { + if (eventData is SubscriptionValidationEventData subscriptionValidationEventData) + { + var responseData = new SubscriptionValidationResponse + { + ValidationResponse = subscriptionValidationEventData.ValidationCode + }; + return Results.Ok(responseData); + } + if (eventData is AcsIncomingCallEventData incomingCallEventData) + { + msgLog.AppendLine($"Event received: {eventGridEvent.EventType}"); + string + fromCallerId = + acsIdentity = incomingCallEventData.FromCommunicationIdentifier.RawId, + toCallerId = incomingCallEventData.ToCommunicationIdentifier.RawId; + + // Lobby Call: Answer + if (toCallerId.Contains(acsGeneratedIdForLobbyCallReceiver) || toCallerId.Contains(acsGeneratedIdForTargetCallReceiver)) + { + #region Answer Call + Uri callbackUri = new(new Uri(callbackUriHost), $"/api/callbacks"); + AnswerCallOptions options = new(incomingCallEventData.IncomingCallContext, callbackUri) + { + OperationContext = !toCallerId.Contains(acsGeneratedIdForTargetCallReceiver) ? "LobbyCall" : "OtherCall", + CallIntelligenceOptions = new CallIntelligenceOptions + { + CognitiveServicesEndpoint = new Uri(cognitiveServiceEndpoint) + } + }; + + AnswerCallResult answerCallResult = await client.AnswerCallAsync(options); + + if (toCallerId.Contains(acsGeneratedIdForTargetCallReceiver)) + { + targetCallConnectionId = answerCallResult.CallConnection.CallConnectionId; + + msgLog.AppendLine($""" + Target Call(Inbound) Answered by Call Automation. + From Caller Raw Id: {fromCallerId} + To Caller Raw Id: {toCallerId} + Target Call Connection Id: {targetCallConnectionId} + Correlation Id: {incomingCallEventData.CorrelationId} + Target Call answered successfully. + """); + } + else + { + lobbyConnectionId = answerCallResult.CallConnection.CallConnectionId; + + msgLog.AppendLine($""" + User Call(Inbound) Answered by Call Automation. + From Caller Raw Id: {fromCallerId} + To Caller Raw Id: {toCallerId} + Lobby Call Connection Id: {lobbyConnectionId} + Correlation Id: {incomingCallEventData.CorrelationId} + Lobby Call answered successfully. + """); + } + #endregion + } + else + { + //msgLog.AppendLine($"Call filtered out - not matching expected scenarios"); + } + } + } + } + var logToSend = msgLog.ToString(); // avoiding multiple logs + msgLog.Clear(); + Console.WriteLine(logToSend); + return Results.Text(logToSend, "text/plain"); + } + catch (Exception ex) + { + Console.WriteLine($"Error occurred: {ex.Message}"); + return Results.Problem(ex.Message, statusCode: StatusCodes.Status500InternalServerError); + } +}); + +#endregion + +#region Callback Handler + +app.MapPost("/api/callbacks", async (CloudEvent[] cloudEvents, ILogger logger) => +{ + StringBuilder msgLog = new(); // to make string builder thread-safe; declared here + try + { + foreach (var cloudEvent in cloudEvents) + { + CallAutomationEventBase parsedEvent = CallAutomationEventParser.Parse(cloudEvent); + var callConnection = client.GetCallConnection(parsedEvent.CallConnectionId); + if (parsedEvent is CallConnected callConnected) + { + Console.WriteLine($"~~~~~~~~~~~~ /api/callbacks ~~~~~~~~~~~~ "); + Console.WriteLine($"Received callConnected.CallConnectionId : {callConnected.CallConnectionId}"); + if ((callConnected.OperationContext ?? string.Empty).Equals("LobbyCall", StringComparison.Ordinal)) + { + // added logs to avoid multiple logs for the same callback + msgLog.AppendLine($""" + ~~~~~~~~~~~~ /api/callbacks ~~~~~~~~~~~~ + Received call event : {parsedEvent.GetType()} + Lobby Call Connection Id: {callConnected.CallConnectionId} + Correlation Id: {callConnected.CorrelationId} + """); + + // record lobby caller id and connection id + CallConnection lobbyCallConnection = client.GetCallConnection(callConnected.CallConnectionId); + CallConnectionProperties callConnectionProperties = lobbyCallConnection.GetCallConnectionProperties(); + lobbyCallerId = callConnectionProperties.Source.RawId; + lobbyConnectionId = callConnectionProperties.CallConnectionId; + Console.WriteLine($""" + Lobby Caller Id: {lobbyCallerId} + Lobby Connection Id: {lobbyConnectionId} + """); + + #region Play lobby waiting message + // setup cognitive service end point + Console.WriteLine($""" + Playing Media to Lobby Call.. + """); + CallMedia callMedia = !string.IsNullOrEmpty(callConnected.CallConnectionId) ? + client.GetCallConnection(callConnected.CallConnectionId).GetCallMedia() + : throw new ArgumentNullException("Call connection id is empty"); + TextSource textSource = + new(textToPlayToLobbyUser) + { + VoiceName = "en-US-NancyNeural" + }; + + List playTo = + new() { new CommunicationUserIdentifier(acsIdentity) }; + + PlayOptions playToOptions = new(playSource: textSource, playTo: playTo) + { + OperationContext = "playToContext" + }; + await callMedia.PlayAsync(playToOptions); + #endregion + } + } + else if (parsedEvent is PlayCompleted playCompleted) + { + // added logs to avoid multiple logs for the same callback + msgLog.AppendLine($""" + ~~~~~~~~~~~~ /api/callbacks ~~~~~~~~~~~~ + Received event: {parsedEvent.GetType()} + """); + + // TODO: Notify Target Cal user + // By pop up in Client app + if (webSocket is null || webSocket.State != WebSocketState.Open) + { + msgLog.AppendLine("ERROR: Web socket is not available."); + return Results.NotFound("Message not sent"); + // throw new ArgumentNullException("web socket is not available."); + } + + // Notify Client + var msg = System.Text.Encoding.UTF8.GetBytes(confirmMessageToTargetCall); + await webSocket.SendAsync(new ArraySegment(msg), WebSocketMessageType.Text, true, CancellationToken.None); + msgLog.AppendLine($"Target Call notified with message: {confirmMessageToTargetCall}"); + return Results.Ok("Target Call notified with message: {confirmMessageToTargetCall}"); + } + else if (parsedEvent is MoveParticipantSucceeded moveParticipantSucceeded) + { + // added logs to avoid multiple logs for the same callback + msgLog.AppendLine($""" + ~~~~~~~~~~~~ /api/callbacks ~~~~~~~~~~~~ + Received event: {parsedEvent.GetType()} + Call Connection Id: {moveParticipantSucceeded.CallConnectionId} + Correlation Id: {moveParticipantSucceeded.CorrelationId} + """); + // move + // Get the updated participants list + msgLog.AppendLine($""" + + ~~~~~~~~~~~~ Participants in Target Connection({targetCallConnectionId}) ~~~~~~~~~~~~ + """); + try + { + CallConnection targetConnection = client.GetCallConnection(moveParticipantSucceeded.CallConnectionId); + var participants = await targetConnection.GetParticipantsAsync(); + + var participantinfo = participants.Value.Select(p => new + { + p.Identifier.RawId, + Type = p.Identifier.GetType().Name, + PhoneNumber = p.Identifier is PhoneNumberIdentifier phone ? phone.PhoneNumber : null, + AcsUserId = p.Identifier is CommunicationUserIdentifier user ? user.Id : null, + }).OrderBy(p => p.AcsUserId) // to display phone numbers first + .Select(p => new + { + Info = string.IsNullOrWhiteSpace(p.AcsUserId) + ? $"{p.Type} - RawId: {p.RawId}, Phone: {p.PhoneNumber}" // extra space for alignment + : $"{p.Type} - RawId: {p.AcsUserId}" + }); + + if (!participantinfo.Any()) + { + Console.WriteLine("No participants found for the specified call connection."); + } + else + { + msgLog.AppendLine($""" + No of Participants: {participantinfo.Count()} + Participants: + ------------- + {string.Join("\n", participantinfo.Select((p, index) => $"{index + 1}. {p.Info}"))} + """); + Console.WriteLine(msgLog.ToString()); + } + } + catch (Exception ex) + { + Console.WriteLine("Error getting participants for call {targetCallConnectionId}: {ex.Message}"); + + } + // end: Get the updated participants list + // end: move + } + else if (parsedEvent is CallDisconnected callDisconnected) + { + // added logs to avoid multiple logs for the same callback + msgLog.AppendLine($""" + ~~~~~~~~~~~~ /api/callbacks ~~~~~~~~~~~~ + Received event: {parsedEvent.GetType()} + Call Connection Id: {callDisconnected.CallConnectionId} + + """); + } + else + { + // msgLog.AppendLine($"Received event: {parsedEvent.GetType()}"); + } + } + // Log the final message + if (0 != msgLog.Length) + { + Console.WriteLine(msgLog.ToString()); + } + return Results.Text((0 == msgLog.Length) ? string.Empty : msgLog.ToString(), "text/plain"); + } + catch (Exception ex) + { + Console.WriteLine($"Error occurred: {ex.Message}"); + return Results.Problem(ex.Message, statusCode: StatusCodes.Status500InternalServerError); + } +}).Produces(StatusCodes.Status200OK); + +#endregion + +#region Lobby Call Support Workflow Endpoints +app.MapPost("/TargetCallToAcsUser(Call Replaced with client app)", async (string acsTarget, ILogger logger) => +{ + StringBuilder msgLog = new(); + msgLog.AppendLine(""" + + ~~~~~~~~~~~~ /TargetCall(Create) ~~~~~~~~~~~~ + """); + + Uri callbackUri = new(new Uri(callbackUriHost), "/api/callbacks"); + CallInvite callInvite = new(new CommunicationUserIdentifier(acsTarget)); + var createCallOptions = new CreateCallOptions(callInvite, callbackUri) + { + CallIntelligenceOptions = new CallIntelligenceOptions + { + CognitiveServicesEndpoint = new Uri("https://cognitive-service-waferwire.cognitiveservices.azure.com/") + } + }; + CreateCallResult createCallResult = await client.CreateCallAsync(createCallOptions); + + targetCallConnectionId = createCallResult.CallConnectionProperties.CallConnectionId; + + msgLog.Append($""" + TargetCall: + ----------- + From: Call Automation + To: {acsTarget} + Target Call Connection Id: {targetCallConnectionId} + Correlation Id: {createCallResult.CallConnectionProperties.CorrelationId} + """); + + Console.WriteLine(msgLog.ToString()); + return Results.Text(msgLog.ToString(), "text/plain"); +}).WithTags("Lobby Call Support APIs"); +app.MapGet("/GetParticipants/{callConnectionId}", async (string callConnectionId, ILogger logger) => +{ + StringBuilder msgLog = new(); + msgLog.AppendLine($""" + + ~~~~~~~~~~~~ /GetParticipants/{callConnectionId} ~~~~~~~~~~~~ + """); + try + { + var callConnection = client.GetCallConnection(callConnectionId); + var participants = await callConnection.GetParticipantsAsync(); + + var participantinfo = participants.Value.Select(p => new + { + p.Identifier.RawId, + Type = p.Identifier.GetType().Name, + PhoneNumber = p.Identifier is PhoneNumberIdentifier phone ? phone.PhoneNumber : null, + AcsUserId = p.Identifier is CommunicationUserIdentifier user ? user.Id : null, + }).OrderBy(p => p.AcsUserId) // to display phone numbers first + .Select(p => new + { + Info = string.IsNullOrWhiteSpace(p.AcsUserId) + ? $"{p.Type} - RawId: {p.RawId}, Phone: {p.PhoneNumber}" // extra space for alignment + : $"{p.Type} - RawId: {p.AcsUserId}" + }); + + if (!participantinfo.Any()) + { + return Results.NotFound(new + { + Message = "No participants found for the specified call connection.", + CallConnectionId = callConnectionId + }); + } + else + { + msgLog.AppendLine($""" + + No of Participants: {participantinfo.Count()} + Participants: + ------------- + {string.Join("\n", participantinfo.Select((p, index) => $"{index + 1}. {p.Info}"))} + """); + Console.WriteLine(msgLog.ToString()); + return Results.Text(msgLog.ToString(), "text/plain"); + } + } + catch (Exception ex) + { + logger.LogError($"Error getting participants for call {callConnectionId}: {ex.Message}"); + return Results.BadRequest(new + { + Error = ex.Message, + CallConnectionId = callConnectionId + }); + } +}).WithTags("Lobby Call Support APIs"); + +#endregion + +#region Websocket implementation +app.UseWebSockets(); +app.Map("/ws", async context => +{ + Console.WriteLine("Received WEB SOCKET request."); + if (context.WebSockets.IsWebSocketRequest) + { + webSocket = await context.WebSockets.AcceptWebSocketAsync(); + var buffer = new byte[1024 * 4]; + + // Keep alive with a read loop + while (webSocket.State == WebSocketState.Open) + { + try + { + var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + var jsResponse = Encoding.UTF8.GetString(buffer, 0, result.Count); + Console.WriteLine($"Received response from Client App: {jsResponse}"); + // Move participant to target call if response is "yes" + + if (result.MessageType == WebSocketMessageType.Close) + { + Console.WriteLine($"result.MessageType: {result.MessageType}"); + await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None); + } + else + { + // Process incoming message or ignore + if (jsResponse.Equals("yes", StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine($"Move Participant operation begins.."); + // Call the Move Participants API + #region Move Participant + try + { + Console.WriteLine($""" + ~~~~~~~~~~~~ /api/callbacks ~~~~~~~~~~~~ + Move Participant operation started.. + Source Caller Id: {lobbyCallerId} + Source Connection Id: {lobbyConnectionId} + Target Connection Id: {targetCallConnectionId} + """); + + // Get the target connection + CallConnection targetConnection = client.GetCallConnection(targetCallConnectionId); + + // Get participants from source connection for reference + CallConnection sourceConnection = client.GetCallConnection(lobbyConnectionId); + + // Create participant identifier based on the input + CommunicationIdentifier participantToMove; + if (lobbyCallerId.StartsWith("+")) + { + // Phone number + participantToMove = new PhoneNumberIdentifier(lobbyCallerId); + } + else + { + // ACS Communication User + participantToMove = new CommunicationUserIdentifier(lobbyCallerId); + } + + var response = await targetConnection.MoveParticipantsAsync(options: new([participantToMove], lobbyConnectionId)); + var rawResponse = response.GetRawResponse(); + if (rawResponse.Status >= 200 && rawResponse.Status <= 299) + { + Console.WriteLine(); + Console.WriteLine("Move Participants operation completed successfully."); + } + else + { + throw new Exception($"Move Participants operation failed with status code: {rawResponse.Status}"); + } + } + catch (Exception ex) + { + Console.WriteLine($"Error in move participants operation: {ex.Message}"); + } + #endregion + + } + } + } + catch (Exception ex) + { + Console.WriteLine("----- Web socket error -----"); + Console.WriteLine(ex.Message); + Console.WriteLine("----- End: Web socket error -----"); + + } + } + } + else + { + context.Response.StatusCode = 400; + } +}); +#endregion + +app.Run(); diff --git a/LobbyCallSupportSample/LobbyCallSupportSample/Properties/launchSettings.json b/LobbyCallSupportSample/LobbyCallSupportSample/Properties/launchSettings.json new file mode 100644 index 00000000..9e23f2cf --- /dev/null +++ b/LobbyCallSupportSample/LobbyCallSupportSample/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:39947", + "sslPort": 44389 + } + }, + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7006;http://localhost:5142", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/LobbyCallSupportSample/LobbyCallSupportSample/Resources/Lobby_Call_Support_Scenario.jpg b/LobbyCallSupportSample/LobbyCallSupportSample/Resources/Lobby_Call_Support_Scenario.jpg new file mode 100644 index 00000000..92225d4e Binary files /dev/null and b/LobbyCallSupportSample/LobbyCallSupportSample/Resources/Lobby_Call_Support_Scenario.jpg differ diff --git a/LobbyCallSupportSample/LobbyCallSupportSample/Resources/client-app-ui.jpg b/LobbyCallSupportSample/LobbyCallSupportSample/Resources/client-app-ui.jpg new file mode 100644 index 00000000..3b461a33 Binary files /dev/null and b/LobbyCallSupportSample/LobbyCallSupportSample/Resources/client-app-ui.jpg differ diff --git a/LobbyCallSupportSample/LobbyCallSupportSample/appsettings.Development.json b/LobbyCallSupportSample/LobbyCallSupportSample/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/LobbyCallSupportSample/LobbyCallSupportSample/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/LobbyCallSupportSample/LobbyCallSupportSample/appsettings.json b/LobbyCallSupportSample/LobbyCallSupportSample/appsettings.json new file mode 100644 index 00000000..ad097d60 --- /dev/null +++ b/LobbyCallSupportSample/LobbyCallSupportSample/appsettings.json @@ -0,0 +1,17 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "acsConnectionString": "ACS_CONNECTION_STRING", + "cognitiveServiceEndpoint": "COGNITIVE_SERVICE_ENDPOINT", + "callbackUriHost": "CALLBACK_URI_HOST", + "textToPlayToLobbyUser": "TEXT_TO_PLAY_TO_LOBBY_USER", + "confirmMessageToTargetCall": "CONFIRM_MESSAGE_TO_TARGET_CALL", + "acsGeneratedIdForLobbyCallReceiver": "ACS_GENERATED_ID_FOR_LOBBY_CALL_RECEIVER", + "acsGeneratedIdForTargetCallReceiver": "ACS_GENERATED_ID_FOR_TARGET_CALL_RECEIVER", + "acsGeneratedIdForTargetCallSender": "ACS_GENERATED_ID_FOR_TARGET_CALL_SENDER" +} diff --git a/LobbyCallSupportSample/LobbyCallSupportSample/readme.md b/LobbyCallSupportSample/LobbyCallSupportSample/readme.md new file mode 100644 index 00000000..909e100a --- /dev/null +++ b/LobbyCallSupportSample/LobbyCallSupportSample/readme.md @@ -0,0 +1,83 @@ +| page_type | languages | products | +| --------- | --------------------------------------- | --------------------------------------------------------------------------- | +| sample |
DotNetJava Script
|
azureazure-communication-services
| + +# Call Automation - Lobby Call Support Sample + +This sample demonstrates how to utilize the Call Automation SDK to implement a Lobby Callscenario. Users initially join a lobby call (Call - I) and remain on hold until an user in the target call (Call - II) confirms their participation. Once approved, the bot automatically moves the lobby user to the target call. + +# Design + + +![Lobby Call Support](./Resources/Lobby_Call_Support_Scenario.jpg) + +## Prerequisites +- An Azure account with an active subscription. [Create an account for free](https://azure.microsoft.com/free/?WT.mc_id=A261C142F). +- A deployed Communication Services resource. [Create a Communication Services resource](https://docs.microsoft.com/azure/communication-services/quickstarts/create-communication-resource). +- A [phone number](https://learn.microsoft.com/en-us/azure/communication-services/quickstarts/telephony/get-phone-number) in your Azure Communication Services resource that can make outbound calls. NB: phone numbers are not available in free subscriptions. +- Create Azure AI Multi Service resource. For details, see [Create an Azure AI Multi service](https://learn.microsoft.com/en-us/azure/cognitive-services/cognitive-services-apis-create-account). +- Create and host a Azure Dev Tunnel. Instructions [here](https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/get-started) +- A Client application that can make calls to the Azure Communication Services resource. This can be a web client or a mobile client. You can use the [Web Client Quickstart](https://github.com/Azure-Samples/communication-services-javascript-quickstarts/tree/users/v-kuppu/LobbyCallConfirmSample) + +## Before running the sample for the first time + +1. Open the web client app at [JS Client Sample](https://github.com/Azure-Samples/communication-services-javascript-quickstarts/tree/users/v-kuppu/LobbyCallConfirmSample) and sign in with your Azure Communication Services identity. +2. Clone the sample repository by running `git clone https://github.com/Azure-Samples/communication-services-javascript-quickstarts.git`. +3. Run the application and observe logs at console, keep this application running. + + ``` + npx webpack serve --config webpack.config.js + ``` +4. UI of client application will be available at `http://localhost:/` and will look like below. + + ![Lobby Call Support](./Resources/client-app-ui.jpg) + + +### Setup and host your Azure DevTunnel + +``` +devtunnel create --allow-anonymous +devtunnel port create -p 7006 +devtunnel host +``` + +### Configuring application + +Open `appSettings.json` file to configure the following settings + +1. `acsConnectionString`: Azure Communication Service resource's connection string. +2. `cognitiveServiceEndpoint`: Cognitive Service resource's endpoint. + - This is used to play media to the participants in the call. + - For more information, see [Create an Azure AI Multi service](https://learn.microsoft.com/en-us/azure/cognitive-services/cognitive-services-apis-create-account). +3. `callbackUriHost`: Base url of the app. (For local development use dev tunnel url) +4. `acsGeneratedIdForLobbyCallReceiver`: ACS Inbound Phone Number +5. `acsGeneratedIdForTargetCallReceiver`: ACS Phone Number to make the first call, external user number in real time +6. `acsGeneratedIdForTargetCallSender`: ACS identity generated using web client + +## Run app locally + +1. Generate an Azure Communication Services identity for the lobby call receiver and target call receiver. You can do this from the Azure Portal(ACS Resource ? Identities & User Access Tokens ? Generate Identity and USER ACCESS TOKEN). +2. Setup EventSubscription(Incoming) with filter for `TO.DATA.RAWID = , `. +3. Setup webhook for Incoming calls to point to `https:///callbacks/incomingcall` in EventSubscription(Incoming). +4. Setup the following keys in the config/constants + ``` + "acsConnectionString": "", + "cognitiveServiceEndpoint": "", + "callbackUriHost": "", + "acsGeneratedIdForLobbyCallReceiver": "",(Generate Voice Calling Identity in Azure Portal) + "acsGeneratedIdForTargetCallReceiver": "",(Generate Voice Calling Identity in Azure Portal) + "acsGeneratedIdForTargetCallSender": "",(Generate Voice Calling Identity in Azure Portal)``` +5. Define a websocket with url as `ws://your-websocket-server-url:port/ws` in your application(program.cs) to send and receive messages from and to the client application. +6. Define a Client application(JS Hero App in this case) that receives and responds to server notifications. Client application is available at `http://localhost:/`. + +7. Start the target call in Client application, + - Add token of target call sender(token would be generated in Azure user & tokens section). + - Add user id of the target call receiver ``. + - Click on `Start Call` button to initiate the call. +8. Expect Call Connected event in /callbacks as the server app answers incoming call from target call sender to target call receiver. +9. Start a call from ACS Test app(angular) to `acsGeneratedIdForLobbyCallReceiver`, call will be answered by the server app and automated voice will be played to lobby user with the text `You are currently in a lobby call, we will notify the admin that you are waiting.` +10. Once the play is completed, Target call will be notified with `A user is waiting in lobby, do you want to add the lobby user to your call?`. +11. Once the Target call confirms from client application, Move `ACS_GENERATED_ID_FOR_LOBBY_CALL_RECEIVER` in the backend sample. +12. If Target user says no, then no MOVE will be performed. +13. Ensure MoveParticipantSucceeded event is received in `/callbacks` endpoint. +14. Ensure the output in the logs shows the the additional lobby user in the target call. The number of participants in the target call are increased by adding the lobby user, then lobby call gets disconnected after the moving the lobbyy user(as lobby user is already moved into the target call). \ No newline at end of file