Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
4948bb4
Lobby Call Support Sample - Initial Checkin without client app modifi…
v-kuppu Jul 21, 2025
a2a36ff
Lobby Call Support Sample - added support for web socket to recdive c…
v-kuppu Jul 28, 2025
cec29b7
LobbyCallSupportSample - testing and code tweaks
v-kuppu Jul 28, 2025
e496346
Lobby Call Support Sample - final checkin
v-kuppu Jul 29, 2025
d13fb43
Updates to Readme.md
v-kuppu Jul 30, 2025
6f97862
Lobby Call Support Sample - removed token dependency for web socket c…
v-kuppu Jul 31, 2025
f2091aa
Lobby Call Support Sample - moved missed sensitive data to config
v-kuppu Aug 4, 2025
cb98145
Lobby Call feature - Readme.md updates and code tweaks
v-kuppu Oct 16, 2025
4f45054
Lobby Call Support - updates to readme.md file
v-kuppu Oct 17, 2025
5571a6e
Readme.md tweaks
v-kuppu Oct 17, 2025
ce297b6
Readme.md tweaks
v-kuppu Oct 17, 2025
c22fc2b
Merge branch 'main' into users/v-kuppu/LobbyCallSupportSampleV2
v-kuppu Oct 23, 2025
e18623e
Lobby Support Sample - addressed comments in PR#228
v-kuppu Oct 23, 2025
208f0c3
Lobby Support Sample - addressed comments in PR#80 from Python Quick …
v-kuppu Oct 23, 2025
6091f97
Lobby Support Sample - addressed comments in Python quick starts PR#80
v-kuppu Oct 24, 2025
d17eed3
Readme.md tweaks
v-kuppu Oct 24, 2025
4233a65
Lobby Call Sample - addressed PR Comments
v-kuppu Oct 24, 2025
2ff1510
Lobby Call Support Sample - Addresssing PR Review comments.
v-kuppu Nov 10, 2025
7e441cf
Lobby Call Support Sample - Simplified programcs and readme.md
v-kuppu Nov 12, 2025
80fbd85
Lobby Call Support Sample - readme.md tweaks
v-kuppu Nov 12, 2025
7031e00
Lobby Call Support Sample - Simplified program.cs and readme.md
v-kuppu Nov 13, 2025
54b43d4
Lobby Call Support Sample - Made code more readable.
v-kuppu Nov 14, 2025
40083c4
Lobby Call Support Sample - readme.md file updates
v-kuppu Nov 14, 2025
f1c6648
Lobby Call Support Sample - removed extra config variables
v-kuppu Nov 18, 2025
c933c07
Lobby Call Support Sample - readme.md updates
v-kuppu Nov 18, 2025
6a7b48e
Lobby Call Support Sample - logged event in /callbacks
v-kuppu Nov 18, 2025
25cfaa8
Lobby Call Support Sample - readme.md updates
v-kuppu Nov 20, 2025
edaad2f
Lobby Call Support Sample - readme.md updates
v-kuppu Nov 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions LobbyCallSupportSample/LobbyCallSupportSample.sln
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Azure.Communication.CallAutomation" Version="1.6.0-beta.1" />
<PackageReference Include="Azure.Messaging.EventGrid" Version="5.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
</ItemGroup>
</Project>
267 changes: 267 additions & 0 deletions LobbyCallSupportSample/LobbyCallSupportSample/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
using Azure.Communication;
using Azure.Communication.CallAutomation;
using Azure.Core;
using Azure.Messaging;
using Azure.Messaging.EventGrid;
using Azure.Messaging.EventGrid.SystemEvents;
using System.Net.WebSockets;
using System.Text;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseHttpsRedirection();

// Configuration helper
string GetConfig(string key) => builder.Configuration[key] ?? throw new ArgumentNullException(paramName: key);

string acsConnectionString = GetConfig(key: "acsConnectionString"),
cognitiveServiceEndpoint = GetConfig(key: "cognitiveServiceEndpoint"),
callbackUriHost = GetConfig(key: "callbackUriHost"),
acsLobbyCallReceiver = GetConfig(key: "acsLobbyCallReceiver"),
acsTargetCallReceiver = GetConfig(key: "acsTargetCallReceiver");

const string confirmationMessage = "A user is waiting in lobby, do you want to add the lobby user to your call?";
const string lobbyMessage = "You are currently in a lobby call, we will notify the admin that you are waiting.";

string acsUserId = string.Empty,
targetCallConnectionId = string.Empty,
lobbyCallConnectionId = string.Empty,
lobbyUserId = string.Empty;

WebSocket? webSocket = null;
CallAutomationClient callAutomationClient = new(connectionString: acsConnectionString);

// Event Handler
app.MapPost("/api/LobbyCallSupportEventHandler", async (EventGridEvent[] events, ILogger<Program> logger) =>
{
logger.LogInformation("~~~ /api/LobbyCallSupportEventHandler ~~~");
try
{
foreach (var eventGridEvent in events)
{
if (!eventGridEvent.TryGetSystemEventData(out var eventData)) continue;
switch (eventData)
{
case SubscriptionValidationEventData validationEvent:
return Results.Ok(value: new SubscriptionValidationResponse { ValidationResponse = validationEvent.ValidationCode });

case AcsIncomingCallEventData incomingCallEvent:
logger.LogInformation("Event: {Type}", eventGridEvent.EventType);
acsUserId = incomingCallEvent.FromCommunicationIdentifier.RawId;
var toIdentifier = incomingCallEvent.ToCommunicationIdentifier.RawId;
if (toIdentifier.Contains(acsLobbyCallReceiver) || toIdentifier.Contains(acsTargetCallReceiver))
{
var callbackUri = new Uri(baseUri: new Uri(callbackUriHost), relativeUri: "/api/callbacks");
var answerCallOptions = new AnswerCallOptions(
incomingCallContext: incomingCallEvent.IncomingCallContext,
callbackUri: callbackUri)
{
OperationContext = !toIdentifier.Contains(acsTargetCallReceiver) ? "LobbyCall" : "OtherCall",
CallIntelligenceOptions = new CallIntelligenceOptions
{
CognitiveServicesEndpoint = new Uri(uriString: cognitiveServiceEndpoint)
}
};
AnswerCallResult answerResult = await callAutomationClient.AnswerCallAsync(options: answerCallOptions);
if (toIdentifier.Contains(acsTargetCallReceiver))
{
targetCallConnectionId = answerResult.CallConnection.CallConnectionId;
logger.LogInformation(
"Target Call Answered. From: {From}, To: {To}, ConnId: {Conn}, CorrId: {Corr}",
acsUserId, toIdentifier, targetCallConnectionId, incomingCallEvent.CorrelationId);
}
else
{
lobbyCallConnectionId = answerResult.CallConnection.CallConnectionId;
logger.LogInformation(
"Lobby Call Answered. From: {From}, To: {To}, ConnId: {Conn}, CorrId: {Corr}",
acsUserId, toIdentifier, lobbyCallConnectionId, incomingCallEvent.CorrelationId);
}
}
break;
}
}
return Results.Ok();
}
catch (Exception ex)
{
logger.LogError(exception: ex, message: "Error");
throw;
}
});

// Callback Handler
app.MapPost("/api/callbacks", async (CloudEvent[] events, ILogger<Program> logger) =>
{
try
{
foreach (var cloudEvent in events)
{
var callEvent = CallAutomationEventParser.Parse(cloudEvent);
var callConnection = callAutomationClient.GetCallConnection(callConnectionId: callEvent.CallConnectionId);
logger.LogInformation("~~~ /api/callbacks ~~~\n Event: {callEvent}", callEvent);
switch (callEvent)
{
case CallConnected callConnected when (callConnected.OperationContext ?? "") == "LobbyCall":
logger.LogInformation("\nCallConnected: {ConnId}", callConnected.CallConnectionId);
CallConnectionProperties callProperties = callConnection.GetCallConnectionProperties();
lobbyUserId = callProperties.Source.RawId;
lobbyCallConnectionId = callProperties.CallConnectionId;
logger.LogInformation("Lobby Caller: {Caller}, Conn: {Conn}", lobbyUserId, lobbyCallConnectionId);
var callMedia = callAutomationClient.GetCallConnection(callConnected.CallConnectionId).GetCallMedia();
var textSource = new TextSource(text: lobbyMessage) { VoiceName = "en-US-NancyNeural" };
await callMedia.PlayAsync(
options: new PlayOptions(
playSource: textSource,
playTo: [new CommunicationUserIdentifier(id: acsUserId)])
{
OperationContext = "playToContext"
});
break;

case PlayCompleted:
logger.LogInformation("PlayCompleted event");
if (webSocket is null || webSocket.State != WebSocketState.Open)
{
logger.LogError("WebSocket unavailable");
return Results.NotFound(value: "Message not sent");
}
await webSocket.SendAsync(
buffer: Encoding.UTF8.GetBytes(confirmationMessage),
messageType: WebSocketMessageType.Text,
endOfMessage: true,
cancellationToken: CancellationToken.None);
logger.LogInformation("Target notified: {Msg}", confirmationMessage);
return Results.Ok(value: $"Target notified: {confirmationMessage}");

case MoveParticipantSucceeded moveParticipantSucceeded:
logger.LogInformation("MoveParticipantSucceeded: {ConnId}", moveParticipantSucceeded.CallConnectionId);
var targetConnection = callAutomationClient.GetCallConnection(callConnectionId: moveParticipantSucceeded.CallConnectionId);
var participants = await targetConnection.GetParticipantsAsync();
LogParticipants(participants: participants.Value, logger: logger);
break;

case CallDisconnected callDisconnected:
logger.LogInformation("CallDisconnected: {ConnId}", callDisconnected.CallConnectionId);
break;
}
}
return Results.Ok();
}
catch (Exception ex)
{
logger.LogError(exception: ex, message: "Error");
throw;
}
}).Produces(statusCode: 200);

// Get Participants
app.MapGet("/GetParticipants/{connId}", async (string connId, ILogger<Program> logger) =>
{
logger.LogInformation("~~~ /GetParticipants/{ConnId} ~~~", connId);
try
{
var callConnection = callAutomationClient.GetCallConnection(callConnectionId: connId);
var participants = await callConnection.GetParticipantsAsync();
if (!participants.Value.Any())
return Results.NotFound(value: new { Message = "No participants found.", CallConnectionId = connId });
LogParticipants(participants: participants.Value, logger: logger);
return Results.Ok();
}
catch (Exception ex)
{
logger.LogError("Error getting participants: {Msg}", ex.Message);
throw;
}
}).WithTags("Lobby Call Support APIs");

// WebSocket
app.UseWebSockets();
app.Map("/ws", async context =>
{
var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
if (!context.WebSockets.IsWebSocketRequest)
{
context.Response.StatusCode = 400;
return;
}
webSocket = await context.WebSockets.AcceptWebSocketAsync();
var buffer = new byte[4096];
while (webSocket.State == WebSocketState.Open)
{
try
{
var receiveResult = await webSocket.ReceiveAsync(
buffer: new ArraySegment<byte>(buffer),
cancellationToken: CancellationToken.None);
var clientMessage = Encoding.UTF8.GetString(buffer, 0, receiveResult.Count);
logger.LogInformation("Client response: {Msg}", clientMessage);

if (receiveResult.MessageType == WebSocketMessageType.Close)
{
await webSocket.CloseAsync(
closeStatus: WebSocketCloseStatus.NormalClosure,
statusDescription: "Closing",
cancellationToken: CancellationToken.None);
}
else if (clientMessage.Equals("yes", StringComparison.OrdinalIgnoreCase))
{
logger.LogInformation("Move Participant...");
try
{
var targetConnection = callAutomationClient.GetCallConnection(callConnectionId: targetCallConnectionId);
CommunicationIdentifier participant = lobbyUserId.StartsWith("+")
? new PhoneNumberIdentifier(phoneNumber: lobbyUserId)
: new CommunicationUserIdentifier(id: lobbyUserId);
var moveResult = await targetConnection.MoveParticipantsAsync(
options: new MoveParticipantsOptions(
targetParticipants: [participant],
fromCall: lobbyCallConnectionId));
var rawResponse = moveResult.GetRawResponse();
if (rawResponse.Status is >= 200 and <= 299)
logger.LogInformation("Move Participant operation is initiated.");
else
throw new Exception(message: $"Move failed: {rawResponse.Status}");
}
catch (Exception ex)
{
logger.LogError("Move error: {Msg}", ex.Message);
throw;
}
}
}
catch (Exception ex)
{
logger.LogError("WebSocket error: {Msg}", ex.Message);
}
}
});

app.Run();

// Helper: Log participants
static void LogParticipants(IEnumerable<CallParticipant> participants, ILogger logger)
{
var participantInfo = participants.Select(participant => participant.Identifier switch
{
PhoneNumberIdentifier phone => $"Phone - RawId: {phone.RawId}, Phone: {phone.PhoneNumber}",
CommunicationUserIdentifier user => $"ACSUser - RawId: {user.Id}",
_ => $"{participant.Identifier.GetType().Name} - RawId: {participant.Identifier.RawId}"
}).ToList();

logger.LogInformation(
"Participants ({Count}):\n{List}",
participantInfo.Count,
string.Join("\n", participantInfo.Select((info, index) => $"{index + 1}. {info}")));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7006;http://localhost:5143",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
14 changes: 14 additions & 0 deletions LobbyCallSupportSample/LobbyCallSupportSample/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"acsConnectionString": "<ACS_CONNECTION_STRING>",
"cognitiveServiceEndpoint": "<COGNITIVE_SERVICE_ENDPOINT>",
"callbackUriHost": "<CALLBACK_URI_HOST>",
"acsLobbyCallReceiver": "<ACS_LOBBY_CALL_RECEIVER>",
"acsTargetCallReceiver": "<ACS_TARGET_CALL_RECEIVER>"
}
Loading