Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
241 changes: 241 additions & 0 deletions CallRecording/Controllers/RecordingsController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
using Azure.Communication;
using Azure.Communication.CallAutomation;
using Azure.Messaging;
using Azure.Messaging.EventGrid;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System;
using System.Threading.Tasks;

namespace RecordingApi.Controllers
{
/// <summary>
/// Recording APIs
/// </summary>
[ApiController]
public class RecordingsController : ControllerBase
{
private readonly ILogger _logger;
private readonly CallAutomationClient _client;
private readonly IConfiguration _configuration;

// for simplicity using static values
private static string _serverCallId = "";
private static string _callConnectionId = "";
private static string _recordingId = "";
private static string _contentLocation = "";
private static string _deleteLocation = "";

/// <summary>
/// Initilize Recording
/// </summary>
/// <param name="configuration"></param>
/// <param name="logger"></param>
public RecordingsController(IConfiguration configuration, ILogger<RecordingsController> logger)
{
_logger = logger;
_configuration = configuration;
_client = new CallAutomationClient(_configuration["ACSResourceConnectionString"]);
}

#region outbound call - an active call required for recording to start.

/// <summary>
/// Start outbound call, Run before start recording
/// </summary>
/// <param name="targetPhoneNumber"></param>
/// <returns></returns>
[HttpGet("OutboundCall")]
public async Task<IActionResult> OutboundCall([FromQuery] string targetPhoneNumber)
{
targetPhoneNumber = targetPhoneNumber.Replace(" ", "+");
var callerId = new PhoneNumberIdentifier(_configuration["ACSAcquiredPhoneNumber"]);
var target = new PhoneNumberIdentifier(targetPhoneNumber);
var callInvite = new CallInvite(target, callerId);
var createCallOption = new CreateCallOptions(callInvite, new Uri(_configuration["BaseUri"] + "/api/callbacks"));

var response = await _client.CreateCallAsync(createCallOption).ConfigureAwait(false);
_callConnectionId = response.Value.CallConnection.CallConnectionId;

return Ok($"CallConnectionId: {_callConnectionId}");
}

#endregion

/// <summary>
/// Start Recording
/// </summary>
/// <param name="serverCallId"></param>
/// <returns></returns>
[HttpGet("StartRecording")]
public async Task<IActionResult> StartRecordingAsync([FromQuery] string serverCallId)
{
try
{
_serverCallId = serverCallId ?? _client.GetCallConnection(_callConnectionId).GetCallConnectionProperties().Value.ServerCallId;
StartRecordingOptions recordingOptions = new StartRecordingOptions(new ServerCallLocator(_serverCallId));
var callRecording = _client.GetCallRecording();
var response = await callRecording.StartAsync(recordingOptions).ConfigureAwait(false);
_recordingId = response.Value.RecordingId;
return Ok($"RecordingId: {_recordingId}");
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}

/// <summary>
/// Pause Recording
/// </summary>
/// <param name="recordingId"></param>
/// <returns></returns>
[HttpPost("PauseRecording")]
public async Task<IActionResult> PauseRecording([FromQuery] string recordingId)
{
_recordingId = recordingId ?? _recordingId;
var response = await _client.GetCallRecording().PauseAsync(_recordingId).ConfigureAwait(false);

_logger.LogInformation($"Pause Recording response -- > {response}");
return Ok();
}

/// <summary>
/// Resume Recording
/// </summary>
/// <param name="recordingId"></param>
/// <returns></returns>
[HttpPost("ResumeRecording")]
public async Task<IActionResult> ResumeRecordingAsync([FromQuery] string recordingId)
{
_recordingId = recordingId ?? _recordingId;
var response = await _client.GetCallRecording().ResumeAsync(_recordingId).ConfigureAwait(false);

_logger.LogInformation($"Resume Recording response -- > {response}");
return Ok();
}

/// <summary>
/// Stop Recording
/// </summary>
/// <param name="recordingId"></param>
/// <returns></returns>
[HttpDelete("StopRecording")]
public async Task<IActionResult> StopRecordingAsync([FromQuery] string recordingId)
{
_recordingId = recordingId ?? _recordingId;
var response = await _client.GetCallRecording().StopAsync(_recordingId).ConfigureAwait(false);

_logger.LogInformation($"StopRecordingAsync response -- > {response}");
return Ok();
}

/// <summary>
/// Get recording state
/// </summary>
/// <param name="recordingId"></param>
/// <returns></returns>
[HttpGet("GetRecordingState")]
public async Task<IActionResult> GetRecordingStateAsync([FromQuery] string recordingId)
{
_recordingId = recordingId ?? _recordingId;
var response = await _client.GetCallRecording().GetStateAsync(_recordingId).ConfigureAwait(false);

_logger.LogInformation($"GetRecordingStateAsync response -- > {response}");
return Ok();
}

/// <summary>
/// Download Recording
/// </summary>
/// <returns></returns>
[HttpGet("DownloadRecording")]
public IActionResult DownloadRecording()
{
var callRecording = _client.GetCallRecording();
callRecording.DownloadTo(new Uri(_contentLocation), "Recording_File.wav");
return Ok();
}

/// <summary>
/// Delete Recording
/// </summary>
/// <returns></returns>
[HttpDelete("DeleteRecording")]
public IActionResult DeleteRecording()
{
_client.GetCallRecording().Delete(new Uri(_deleteLocation));
return Ok();
}

#region call backs apis

/// <summary>
/// Web hook to receive the recording file update status event, [Do not call directly from Swagger]
/// </summary>
/// <param name="eventGridEvents"></param>
/// <returns></returns>
[HttpPost]
[Route("recordingFileStatus")]
public IActionResult RecordingFileStatus([FromBody] EventGridEvent[] eventGridEvents)
{
foreach (var eventGridEvent in eventGridEvents)
{
if (eventGridEvent.TryGetSystemEventData(out object eventData))
{
// Handle the webhook subscription validation event.
if (eventData is Azure.Messaging.EventGrid.SystemEvents.SubscriptionValidationEventData subscriptionValidationEventData)
{
var responseData = new Azure.Messaging.EventGrid.SystemEvents.SubscriptionValidationResponse
{
ValidationResponse = subscriptionValidationEventData.ValidationCode
};
return Ok(responseData);
}

if (eventData is Azure.Messaging.EventGrid.SystemEvents.AcsRecordingFileStatusUpdatedEventData statusUpdated)
{
_contentLocation = statusUpdated.RecordingStorageInfo.RecordingChunks[0].ContentLocation;
_deleteLocation = statusUpdated.RecordingStorageInfo.RecordingChunks[0].DeleteLocation;
}
}
}
return Ok($"Recording Download Location : {_contentLocation}, Recording Delete Location: {_deleteLocation}");
}

/// <summary>
/// Call backs for signalling events, [Do not call directly from swagger]
/// </summary>
/// <param name="cloudEvents"></param>
/// <returns></returns>
[HttpPost]
[Route("/api/callbacks")]
public IActionResult Callbacks([FromBody] CloudEvent[] cloudEvents)
{
try
{
foreach (var cloudEvent in cloudEvents)
{
_logger.LogInformation($"Event received: {JsonConvert.SerializeObject(cloudEvent)}");
CallAutomationEventBase @event = CallAutomationEventParser.Parse(cloudEvent);

// for start recording we required server call id, so capture it when call connected.
if (@event is CallConnected)
{
_logger.LogInformation($"Server Call Id: {@event.ServerCallId}");
break;
}
}
}
catch (Exception ex)
{
return BadRequest(new { Exception = ex });
}
return Ok();
}

#endregion
}
}
28 changes: 28 additions & 0 deletions CallRecording/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.IO;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddSwaggerGen(c =>
{
c.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, "RecordingApi.xml"));
});

var app = builder.Build();

app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "API V1");
});

app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.MapControllers();

app.Run();
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"QuickStartApi": {
"RecordingApi": {
"commandName": "Project",
"launchBrowser": true,
"applicationUrl": "https://localhost:5001;http://localhost:5000",
Expand Down
82 changes: 82 additions & 0 deletions CallRecording/README.MD
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
---
Page Type: Sample
Languages: C#
Products: Azure.Communication.CallAutomation
---

# Recording APIs Sample

This is a sample application to showcase how the Call Automation SDK can be used to add recording features to any application.
This application, built on the .NET Core framework using C#, is seamlessly integrated with Azure Communication Services. It harnesses the power of Azure Communication Services to establish connections and enable communication features within the application.
A separate branch with end to end implementation is [available](https://github.com/Azure-Samples/communication-services-web-calling-hero/tree/public-preview). It's a public preview branch and uses beta SDKs that are not meant for production use. Please use the main branch sample for any production scenarios.

## Prerequisites
- Create an Azure account with an active subscription. For details, see [Create an account for free](https://azure.microsoft.com/free/)
- [Visual Studio (2022 and above)](https://visualstudio.microsoft.com/vs/)
- [.NET7](https://dotnet.microsoft.com/en-us/download/dotnet/7.0) (Make sure to install version that corresponds with your visual studio instance, 32 vs 64 bit)
- 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 Communication Service resource **Connection string** under Keys section for this sample.
- Enable Visual studio dev tunneling for local development. For details, see [Enable dev tunnel] (https://learn.microsoft.com/en-us/connectors/custom-connectors/port-tunneling)

- To enable dev tunneling, Click `Tools` -> `Options` in Visual Studio 2022. In the search bar type tunnel, Click the checkbox under `Environment` -> `Preview Features` called `Enable dev tunnels for Web Application`
![EnableDevTunnel](./data/EnableDevTunnel.png)
- Create `Dev Tunnels` by followingg this link -> [Dev Tunnels.](https://learn.microsoft.com/en-us/aspnet/core/test/dev-tunnels?view=aspnetcore-7.0)
**Note:** For accessing the Dev Tunnel Url publicly change the Access to Public.


## Clone the code local and update appsettings

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. Run `git clone https://github.com/Azure-Samples/Communication-Services-dotnet-quickstarts.git`
3. Once you get the code on local machine, navigate to **CallRecording/appsettings.json** file found under the CallRecording folder.
4. Update the values for below.

| Key | Value | Description |
| -------- | -------- | -------- |
| `ACSResourceConnectionString` | \<ACS Connection String> | Input your ACS connection string in the variable |
| `ACSAcquiredPhoneNumber` | \<ACS Acquired Number> | Phone number associated with the Azure Communication Service resource |
| `BaseUri` | \<dev tunnel url> | Base url of the app, don't add `/` at end. For getting the dev tunnel url, run the app once. |

## Locally Run the sample app

1. Go to CallRecording folder and open `RecordingApi.csproj` solution in Visual Studio.
2. Enable Visual Studio Dev Tunnels for local development (see pre-requisite section for enabling dev tunnel).
3. Run the application in debug mode, swagger url should open, if swagger not opened by itself add `/swagger/index.html` at the end.

## Create Webhook for Microsoft.Communication.RecordingFileStatus event
Call Recording enables you to record multiple calling scenarios available in Azure Communication Services by providing you with a set of APIs to start, stop, pause and resume recording. To learn more about it, see [this guide](https://learn.microsoft.com/en-us/azure/communication-services/concepts/voice-video-calling/call-recording).
1. Navigate to your Communication Service resource on Azure portal and select `Events` from the left side blade.
2. Click `+ Event Subscription` to create a new subscription, provide `Name` field value.
3. Under Topic details, choose a System Topic or create new, no changes required if its already have topic name.
4. Under `Event Types` Filter for `Recording File Status Updated` event.
5. Choose `Endpoint Type` as `Web Hook` and provide the public url generated by Dev Tunnels. It would look like `https://21pdf6lm-44348.usw2.devtunnels.ms/recordingFileStatus`.
6. Click `Create` to complete the event grid subscription. The subscription is ready when the provisioning status is marked as succeeded.
**Note:** Application should be running to able to create the `Web Hook` successfully.


# Step by step guid for testing recording APIs via swagger.

Once App is running local, you will see all list of exposed API on swagger.
1. Step 1. Start a call invoke OutboundCall under Outbound section.
- Try it out `GET OutboundCall`, provide the Target PSTN phone number to get the call.
- `Execute`, accept the call on Target PSTN Phone number, Keep call running.
2. Step 2. Start Recording.
- Try it out `GET StartRecording`, provide the serverCallId value, optional if recording the same call started in step1.
- `Execute`, recording would be started.
3. Step 3. (Optional) Execute `POST PauseRecording` and then `POST ResumeRecording`, passing recordingId is optional.
4. Step 4. Execute `DELETE StopReocording` for stop the recording.
5. Step 5. Execute `GET DownloadRecording` for downloading the recording from server, only last recorded file will be downloaded.
6. Step 6. Execute `DELETE DeleteRecording` for delete the recording at server.


## Troubleshooting

1. Solution doesn\'t build, it throws errors during build.
- Clean/rebuild the C# solution.
2. Recording files not getting downloaded.
- Check for webhook settings if dev tunnel url is correct.

## Additional Reading

- [Azure Communication Calling SDK](https://docs.microsoft.com/azure/communication-services/concepts/voice-video-calling/calling-sdk-features) - To learn more about the calling web SDK.
- [ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/introduction-to-aspnet-core?view=aspnetcore-7.0) - Framework for building web applications

Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
<AutoGenerateBindingRedirects>false</AutoGenerateBindingRedirects>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Communication.CallAutomation" Version="1.0.0-beta.1" />
<PackageReference Include="Azure.Communication.Identity" Version="1.2.0" />
<PackageReference Include="Azure.Communication.CallAutomation" Version="1.0.0" />
<PackageReference Include="Azure.Messaging.EventGrid" Version="4.12.0" />
<PackageReference Include="Azure.Storage.Blobs" Version="12.14.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup>
</Project>

File renamed without changes.
Binary file added CallRecording/Recording_File.wav
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
},
"AllowedHosts": "*",
"ACSResourceConnectionString": "%ACSResourceConnectionString%",
"BlobStorageConnectionString": "%BlobStorageConnectionString%",
"BlobContainerName": "%BlobContainerName%"
"ACSAcquiredPhoneNumber": "%ACSAcquiredPhoneNumber%",
"BaseUri": "%BaseUri%"
}
Binary file added CallRecording/data/EnableDevTunnel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading