diff --git a/CallRecording/Controllers/RecordingsController.cs b/CallRecording/Controllers/RecordingsController.cs new file mode 100644 index 00000000..3fa033d2 --- /dev/null +++ b/CallRecording/Controllers/RecordingsController.cs @@ -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 +{ + /// + /// Recording APIs + /// + [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 = ""; + + /// + /// Initilize Recording + /// + /// + /// + public RecordingsController(IConfiguration configuration, ILogger logger) + { + _logger = logger; + _configuration = configuration; + _client = new CallAutomationClient(_configuration["ACSResourceConnectionString"]); + } + + #region outbound call - an active call required for recording to start. + + /// + /// Start outbound call, Run before start recording + /// + /// + /// + [HttpGet("OutboundCall")] + public async Task 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 + + /// + /// Start Recording + /// + /// + /// + [HttpGet("StartRecording")] + public async Task 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); + } + } + + /// + /// Pause Recording + /// + /// + /// + [HttpPost("PauseRecording")] + public async Task PauseRecording([FromQuery] string recordingId) + { + _recordingId = recordingId ?? _recordingId; + var response = await _client.GetCallRecording().PauseAsync(_recordingId).ConfigureAwait(false); + + _logger.LogInformation($"Pause Recording response -- > {response}"); + return Ok(); + } + + /// + /// Resume Recording + /// + /// + /// + [HttpPost("ResumeRecording")] + public async Task ResumeRecordingAsync([FromQuery] string recordingId) + { + _recordingId = recordingId ?? _recordingId; + var response = await _client.GetCallRecording().ResumeAsync(_recordingId).ConfigureAwait(false); + + _logger.LogInformation($"Resume Recording response -- > {response}"); + return Ok(); + } + + /// + /// Stop Recording + /// + /// + /// + [HttpDelete("StopRecording")] + public async Task StopRecordingAsync([FromQuery] string recordingId) + { + _recordingId = recordingId ?? _recordingId; + var response = await _client.GetCallRecording().StopAsync(_recordingId).ConfigureAwait(false); + + _logger.LogInformation($"StopRecordingAsync response -- > {response}"); + return Ok(); + } + + /// + /// Get recording state + /// + /// + /// + [HttpGet("GetRecordingState")] + public async Task GetRecordingStateAsync([FromQuery] string recordingId) + { + _recordingId = recordingId ?? _recordingId; + var response = await _client.GetCallRecording().GetStateAsync(_recordingId).ConfigureAwait(false); + + _logger.LogInformation($"GetRecordingStateAsync response -- > {response}"); + return Ok(); + } + + /// + /// Download Recording + /// + /// + [HttpGet("DownloadRecording")] + public IActionResult DownloadRecording() + { + var callRecording = _client.GetCallRecording(); + callRecording.DownloadTo(new Uri(_contentLocation), "Recording_File.wav"); + return Ok(); + } + + /// + /// Delete Recording + /// + /// + [HttpDelete("DeleteRecording")] + public IActionResult DeleteRecording() + { + _client.GetCallRecording().Delete(new Uri(_deleteLocation)); + return Ok(); + } + + #region call backs apis + + /// + /// Web hook to receive the recording file update status event, [Do not call directly from Swagger] + /// + /// + /// + [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}"); + } + + /// + /// Call backs for signalling events, [Do not call directly from swagger] + /// + /// + /// + [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 + } +} diff --git a/CallRecording/Program.cs b/CallRecording/Program.cs new file mode 100644 index 00000000..2a91bdde --- /dev/null +++ b/CallRecording/Program.cs @@ -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(); diff --git a/ServerRecording/Properties/launchSettings.json b/CallRecording/Properties/launchSettings.json similarity index 92% rename from ServerRecording/Properties/launchSettings.json rename to CallRecording/Properties/launchSettings.json index 3ae1b7b1..4b02a8ab 100644 --- a/ServerRecording/Properties/launchSettings.json +++ b/CallRecording/Properties/launchSettings.json @@ -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", diff --git a/CallRecording/README.MD b/CallRecording/README.MD new file mode 100644 index 00000000..93454ea1 --- /dev/null +++ b/CallRecording/README.MD @@ -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` | \ | Input your ACS connection string in the variable | + | `ACSAcquiredPhoneNumber` | \ | Phone number associated with the Azure Communication Service resource | + | `BaseUri` | \ | 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 + diff --git a/ServerRecording/RecordingApi.csproj b/CallRecording/RecordingApi.csproj similarity index 63% rename from ServerRecording/RecordingApi.csproj rename to CallRecording/RecordingApi.csproj index 9f0d929f..f2bfb19d 100644 --- a/ServerRecording/RecordingApi.csproj +++ b/CallRecording/RecordingApi.csproj @@ -1,14 +1,14 @@  - net6.0 + net7.0 false + True - - + - + \ No newline at end of file diff --git a/ServerRecording/RecordingApi.sln b/CallRecording/RecordingApi.sln similarity index 100% rename from ServerRecording/RecordingApi.sln rename to CallRecording/RecordingApi.sln diff --git a/CallRecording/Recording_File.wav b/CallRecording/Recording_File.wav new file mode 100644 index 00000000..ec724ab8 Binary files /dev/null and b/CallRecording/Recording_File.wav differ diff --git a/ServerRecording/appsettings.json b/CallRecording/appsettings.json similarity index 67% rename from ServerRecording/appsettings.json rename to CallRecording/appsettings.json index c5fcc24c..9bfb4ce8 100644 --- a/ServerRecording/appsettings.json +++ b/CallRecording/appsettings.json @@ -8,6 +8,6 @@ }, "AllowedHosts": "*", "ACSResourceConnectionString": "%ACSResourceConnectionString%", - "BlobStorageConnectionString": "%BlobStorageConnectionString%", - "BlobContainerName": "%BlobContainerName%" + "ACSAcquiredPhoneNumber": "%ACSAcquiredPhoneNumber%", + "BaseUri": "%BaseUri%" } \ No newline at end of file diff --git a/CallRecording/data/EnableDevTunnel.png b/CallRecording/data/EnableDevTunnel.png new file mode 100644 index 00000000..8fc6fd24 Binary files /dev/null and b/CallRecording/data/EnableDevTunnel.png differ diff --git a/ServerRecording/BlobStorageHelper.cs b/ServerRecording/BlobStorageHelper.cs deleted file mode 100644 index 39331da5..00000000 --- a/ServerRecording/BlobStorageHelper.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System; -using System.IO; -using System.Threading.Tasks; -using Azure.Storage.Blobs; -using Azure.Storage.Blobs.Models; - -namespace QuickStartApi -{ - public static class BlobStorageHelper - { - /// - /// Method to check if Blob Storage Container exists or not - /// - /// Connection String details for Azure Blob Storage - /// Container Name to upload files - /// Boolean - public static async Task IsContainerAvailableAsync(string connectionString, string containerName) - { - try - { - BlobServiceClient blobServiceClient = new BlobServiceClient(connectionString); - BlobContainerClient blobContainerClient = blobServiceClient.GetBlobContainerClient(containerName); - return await blobContainerClient.ExistsAsync(); - } - catch (Exception ex) - { - throw new Exception($"Exception checking the container availability. Exception: {ex.Message}"); - } - } - /// - /// Method to check if Blob exists or not - /// - /// Connection String details for Azure Blob Storage - /// Container Name to upload files - /// File name - /// Boolean - public static async Task IsBlobAvailableAsync(string connectionString, string containerName, string blobName) - { - try - { - if (!await IsContainerAvailableAsync(connectionString, containerName)) return false; - BlobClient blobClient = new BlobClient(connectionString, containerName, blobName); - return await blobClient.ExistsAsync(); - } - catch (Exception ex) - { - throw new Exception($"Exception checking the blob availability. Exception: {ex.Message}"); - } - } - /// - /// Method to upload a file to Blob storage - /// - /// Connection String details for Azure Blob Storage - /// Container Name to upload files - /// File name - /// File path of the file to upload - /// BlobStorageHelperInfo - public static async Task UploadFileAsync(string connectionString, string containerName, string blobName, string filePath) - { - BlobStorageHelperInfo blobStorageHelperInfo = new BlobStorageHelperInfo(); - try - { - //checking if container is available - if (!await IsContainerAvailableAsync(connectionString, containerName)) - { - blobStorageHelperInfo.Message = $"Container {containerName} is not available"; - blobStorageHelperInfo.Status = false; - return blobStorageHelperInfo; - } - - //checking if blob is already available - if (await IsBlobAvailableAsync(connectionString, containerName, blobName)) - { - blobStorageHelperInfo.Message = $"Blob \"{blobName}\" already exists"; - blobStorageHelperInfo.Status = false; - return blobStorageHelperInfo; - } - - //Upload blob - BlobServiceClient blobServiceClient = new BlobServiceClient(connectionString); - BlobContainerClient blobContainerClient = blobServiceClient.GetBlobContainerClient(containerName); - BlobClient blobClient = blobContainerClient.GetBlobClient(blobName); - - FileStream uploadFileStream = File.OpenRead(filePath); - BlobContentInfo status = await blobClient.UploadAsync(uploadFileStream, true); - uploadFileStream.Close(); - blobStorageHelperInfo.Message = $"File uploaded successfully. Uri : {blobClient.Uri}"; - blobStorageHelperInfo.Status = true; - return blobStorageHelperInfo; - } - catch (Exception ex) - { - throw new Exception($"The file upload was not successful. Exception: {ex.Message}"); - } - } - } -} diff --git a/ServerRecording/Controller/CallRecordingController.cs b/ServerRecording/Controller/CallRecordingController.cs deleted file mode 100644 index 0c62b287..00000000 --- a/ServerRecording/Controller/CallRecordingController.cs +++ /dev/null @@ -1,420 +0,0 @@ -using Azure.Communication.CallAutomation; -using Microsoft.AspNetCore.Mvc; -using Azure.Messaging.EventGrid; -using Azure.Messaging.EventGrid.SystemEvents; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Newtonsoft.Json; - -namespace QuickStartApi.Controllers -{ - [Route("/recording")] - public class CallRecordingController : Controller - { - private readonly string blobStorageConnectionString; - private readonly string containerName; - private readonly CallAutomationClient callAutomationClient; - private const string CallRecodingActiveErrorCode = "8553"; - private const string CallRecodingActiveError = "Recording is already in progress, one recording can be active at one time."; - public ILogger Logger { get; } - static Dictionary recordingData = new Dictionary(); - public static string recFileFormat; - - public CallRecordingController(IConfiguration configuration, ILogger logger) - { - blobStorageConnectionString = configuration["BlobStorageConnectionString"]; - containerName = configuration["BlobContainerName"]; - callAutomationClient = new CallAutomationClient(configuration["ACSResourceConnectionString"]); - Logger = logger; - } - - /// - /// Method to start call recording - /// - /// Conversation id of the call - [HttpGet] - [Route("startRecording")] - public async Task StartRecordingAsync(string serverCallId) - { - try - { - if (!string.IsNullOrEmpty(serverCallId)) - { - //Passing RecordingContent initiates recording in specific format. audio/audiovideo - //RecordingChannel is used to pass the channel type. mixed/unmixed - //RecordingFormat is used to pass the format of the recording. mp4/mp3/wav - StartRecordingOptions recordingOptions = new StartRecordingOptions(new ServerCallLocator(serverCallId)); - var startRecordingResponse = await callAutomationClient.GetCallRecording() - .StartRecordingAsync(recordingOptions).ConfigureAwait(false); - - Logger.LogInformation($"StartRecordingAsync response -- > {startRecordingResponse.GetRawResponse()}, Recording Id: {startRecordingResponse.Value.RecordingId}"); - - var recordingId = startRecordingResponse.Value.RecordingId; - if (!recordingData.ContainsKey(serverCallId)) - { - recordingData.Add(serverCallId, string.Empty); - } - recordingData[serverCallId] = recordingId; - - return Json(recordingId); - } - else - { - return BadRequest(new { Message = "serverCallId is invalid" }); - } - } - catch (Exception ex) - { - if (ex.Message.Contains(CallRecodingActiveErrorCode)) - { - return BadRequest(new { Message = CallRecodingActiveError }); - } - return Json(new { Exception = ex }); - } - } - - -//********** Replace above API with this if you want to start recording with additional arguments. ************* - - /// - /// Method to start call recording using given parameters - /// - /// Conversation id of the call - /// Recording content type. audiovideo/audio - /// Recording channel type. mixed/unmixed - /// Recording format type. mp3/mp4/wav - //[HttpGet] - //[Route("startRecording")] - public async Task StartRecordingAsync(string serverCallId, string recordingContent, string recordingChannel, string recordingFormat) - { - try - { - if (!string.IsNullOrEmpty(serverCallId)) - { - RecordingContent recContentVal; - RecordingChannel recChannelVal; - RecordingFormat recFormatVal; - - //Passing RecordingContent initiates recording in specific format. audio/audiovideo - //RecordingChannel is used to pass the channel type. mixed/unmixed - //RecordingFormat is used to pass the format of the recording. mp4/mp3/wav - StartRecordingOptions recordingOptions = new StartRecordingOptions(new ServerCallLocator(serverCallId)); - recordingOptions.RecordingChannel = Mapper.RecordingChannelMap.TryGetValue(recordingChannel, out recChannelVal) ? recChannelVal : RecordingChannel.Mixed; - recordingOptions.RecordingContent = Mapper.RecordingContentMap.TryGetValue(recordingContent, out recContentVal) ? recContentVal : RecordingContent.AudioVideo; - recordingOptions.RecordingFormat = Mapper.RecordingFormatMap.TryGetValue(recordingFormat, out recFormatVal) ? recFormatVal : RecordingFormat.Mp4; - - var startRecordingResponse = await callAutomationClient.GetCallRecording() - .StartRecordingAsync(recordingOptions).ConfigureAwait(false); - - Logger.LogInformation($"StartRecordingAudioAsync response -- > {startRecordingResponse.GetRawResponse()}, Recording Id: {startRecordingResponse.Value.RecordingId}"); - - var recordingId = startRecordingResponse.Value.RecordingId; - if (!recordingData.ContainsKey(serverCallId)) - { - recordingData.Add(serverCallId, string.Empty); - } - recordingData[serverCallId] = recordingId; - - return Json(recordingId); - } - else - { - return BadRequest(new { Message = "serverCallId is invalid" }); - } - } - catch (Exception ex) - { - if (ex.Message.Contains(CallRecodingActiveErrorCode)) - { - return BadRequest(new { Message = CallRecodingActiveError }); - } - return Json(new { Exception = ex }); - } - } - - /// - /// Method to pause call recording - /// - /// Conversation id of the call - /// Recording id of the call - [HttpGet] - [Route("pauseRecording")] - public async Task PauseRecordingAsync(string serverCallId, string recordingId) - { - try - { - if (!string.IsNullOrEmpty(serverCallId)) - { - if (string.IsNullOrEmpty(recordingId)) - { - recordingId = recordingData[serverCallId]; - } - else - { - if (!recordingData.ContainsKey(serverCallId)) - { - recordingData[serverCallId] = recordingId; - } - } - var pauseRecording = await callAutomationClient.GetCallRecording().PauseRecordingAsync(recordingId); - Logger.LogInformation($"PauseRecordingAsync response -- > {pauseRecording}"); - - return Ok(); - } - else - { - return BadRequest(new { Message = "serverCallId is invalid" }); - } - } - catch (Exception ex) - { - return Json(new { Exception = ex }); - } - } - - /// - /// Method to resume call recording - /// - /// Conversation id of the call - /// Recording id of the call - [HttpGet] - [Route("resumeRecording")] - public async Task ResumeRecordingAsync(string serverCallId, string recordingId) - { - try - { - if (!string.IsNullOrEmpty(serverCallId)) - { - if (string.IsNullOrEmpty(recordingId)) - { - recordingId = recordingData[serverCallId]; - } - else - { - if (!recordingData.ContainsKey(serverCallId)) - { - recordingData[serverCallId] = recordingId; - } - } - var resumeRecording = await callAutomationClient.GetCallRecording().ResumeRecordingAsync(recordingId); - Logger.LogInformation($"ResumeRecordingAsync response -- > {resumeRecording}"); - - return Ok(); - } - else - { - return BadRequest(new { Message = "serverCallId is invalid" }); - } - } - catch (Exception ex) - { - return Json(new { Exception = ex }); - } - } - - /// - /// Method to stop call recording - /// - /// Conversation id of the call - /// Recording id of the call - /// - [HttpGet] - [Route("stopRecording")] - public async Task StopRecordingAsync(string serverCallId, string recordingId) - { - try - { - if (!string.IsNullOrEmpty(serverCallId)) - { - if (string.IsNullOrEmpty(recordingId)) - { - recordingId = recordingData[serverCallId]; - } - else - { - if (!recordingData.ContainsKey(serverCallId)) - { - recordingData[serverCallId] = recordingId; - } - } - - var stopRecording = await callAutomationClient.GetCallRecording().StopRecordingAsync(recordingId).ConfigureAwait(false); - Logger.LogInformation($"StopRecordingAsync response -- > {stopRecording}"); - - if (recordingData.ContainsKey(serverCallId)) - { - recordingData.Remove(serverCallId); - } - return Ok(); - } - else - { - return BadRequest(new { Message = "serverCallId is invalid" }); - } - } - catch (Exception ex) - { - return Json(new { Exception = ex }); - } - } - - /// - /// Method to get recording state - /// - /// Conversation id of the call - /// Recording id of the call - /// - /// CallRecordingProperties - /// RecordingState is {active}, in case of active recording - /// RecordingState is {inactive}, in case if recording is paused - /// 404:Recording not found, if recording was stopped or recording id is invalid. - /// - [HttpGet] - [Route("getRecordingState")] - public async Task GetRecordingState(string serverCallId, string recordingId) - { - try - { - if (!string.IsNullOrEmpty(serverCallId)) - { - if (string.IsNullOrEmpty(recordingId)) - { - recordingId = recordingData[serverCallId]; - } - else - { - if (!recordingData.ContainsKey(serverCallId)) - { - recordingData[serverCallId] = recordingId; - } - } - - var recordingState = await callAutomationClient.GetCallRecording().GetRecordingStateAsync(recordingId).ConfigureAwait(false); - - Logger.LogInformation($"GetRecordingStateAsync response -- > {recordingState}"); - - return Json(recordingState.Value.RecordingState); - } - else - { - return BadRequest(new { Message = "serverCallId is invalid" }); - } - } - catch (Exception ex) - { - return Json(new { Exception = ex }); - } - } - - /// - /// Web hook to receive the recording file update status event - /// - /// - /// - [HttpPost] - [Route("getRecordingFile")] - public async Task GetRecordingFile([FromBody] object request) - { - try - { - var httpContent = new BinaryData(request.ToString()).ToStream(); - EventGridEvent cloudEvent = EventGridEvent.ParseMany(BinaryData.FromStream(httpContent)).FirstOrDefault(); - - if (cloudEvent.EventType == SystemEventNames.EventGridSubscriptionValidation) - { - var eventData = cloudEvent.Data.ToObjectFromJson(); - - Logger.LogInformation("Microsoft.EventGrid.SubscriptionValidationEvent response -- >" + cloudEvent.Data); - - var responseData = new SubscriptionValidationResponse - { - ValidationResponse = eventData.ValidationCode - }; - - if (responseData.ValidationResponse != null) - { - return Ok(responseData); - } - } - - if (cloudEvent.EventType == SystemEventNames.AcsRecordingFileStatusUpdated) - { - Logger.LogInformation($"Event type is -- > {cloudEvent.EventType}"); - - Logger.LogInformation("Microsoft.Communication.RecordingFileStatusUpdated response -- >" + cloudEvent.Data); - - var eventData = cloudEvent.Data.ToObjectFromJson(); - - Logger.LogInformation("Start processing metadata -- >"); - - await ProcessFile(eventData.RecordingStorageInfo.RecordingChunks[0].MetadataLocation, - eventData.RecordingStorageInfo.RecordingChunks[0].DocumentId, - FileFormat.Json, - FileDownloadType.Metadata); - - Logger.LogInformation("Start processing recorded media -- >"); - - await ProcessFile(eventData.RecordingStorageInfo.RecordingChunks[0].ContentLocation, - eventData.RecordingStorageInfo.RecordingChunks[0].DocumentId, - string.IsNullOrWhiteSpace(recFileFormat) ? FileFormat.Mp4 : recFileFormat, - FileDownloadType.Recording); - } - - return Ok(); - } - catch (Exception ex) - { - return Json(new { Exception = ex }); - } - } - - private async Task ProcessFile(string downloadLocation, string documentId, string fileFormat, string downloadType) - { - var recordingDownloadUri = new Uri(downloadLocation); - var response = await callAutomationClient.GetCallRecording().DownloadStreamingAsync(recordingDownloadUri); - - Logger.LogInformation($"Download {downloadType} response -- >" + response.GetRawResponse()); - Logger.LogInformation($"Save downloaded {downloadType} -- >"); - - string filePath = ".\\" + documentId + "." + fileFormat; - using (Stream streamToReadFrom = response.Value) - { - using (Stream streamToWriteTo = System.IO.File.Open(filePath, FileMode.Create)) - { - await streamToReadFrom.CopyToAsync(streamToWriteTo); - await streamToWriteTo.FlushAsync(); - } - } - - if (string.Equals(downloadType, FileDownloadType.Metadata, StringComparison.InvariantCultureIgnoreCase) && System.IO.File.Exists(filePath)) - { - Root deserializedFilePath = JsonConvert.DeserializeObject(System.IO.File.ReadAllText(filePath)); - recFileFormat = deserializedFilePath.recordingInfo.format; - - Logger.LogInformation($"Recording File Format is -- > {recFileFormat}"); - } - - Logger.LogInformation($"Starting to upload {downloadType} to BlobStorage into container -- > {containerName}"); - - var blobStorageHelperInfo = await BlobStorageHelper.UploadFileAsync(blobStorageConnectionString, containerName, filePath, filePath); - if (blobStorageHelperInfo.Status) - { - Logger.LogInformation(blobStorageHelperInfo.Message); - Logger.LogInformation($"Deleting temporary {downloadType} file being created"); - - System.IO.File.Delete(filePath); - } - else - { - Logger.LogError($"{downloadType} file was not uploaded,{blobStorageHelperInfo.Message}"); - } - - return true; - } - } -} \ No newline at end of file diff --git a/ServerRecording/Models.cs b/ServerRecording/Models.cs deleted file mode 100644 index d36133e3..00000000 --- a/ServerRecording/Models.cs +++ /dev/null @@ -1,156 +0,0 @@ -using System; -using System.Collections.Generic; -using Azure.Communication.CallAutomation; - -namespace QuickStartApi -{ - public class BlobStorageHelperInfo - { - public string Message { set; get; } - public bool Status { set; get; } - } - - static class FileDownloadType - { - const string recordingType = "recording"; - const string metadataType = "metadata"; - - public static string Recording - { - get - { - return recordingType; - } - } - - public static string Metadata - { - get - { - return metadataType; - } - } - } - - static class FileFormat - { - const string json = "json"; - const string mp4 = "mp4"; - const string mp3 = "mp3"; - const string wav = "wav"; - - public static string Json - { - get - { - return json; - } - } - - public static string Mp4 - { - get - { - return mp4; - } - } - - public static string Mp3 - { - get - { - return mp3; - } - } - - public static string Wav - { - get - { - return wav; - } - } - } - - public class Mapper - { - static Dictionary recContentMap - = new Dictionary() - { - { "audiovideo", RecordingContent.AudioVideo }, - { "audio", RecordingContent.Audio } - }; - - static Dictionary recChannelMap - = new Dictionary() - { - { "mixed", RecordingChannel.Mixed }, - { "unmixed", RecordingChannel.Unmixed } - }; - - static Dictionary recFormatMap - = new Dictionary() - { - { "mp3", RecordingFormat.Mp3 }, - { "mp4", RecordingFormat.Mp4 }, - { "wav", RecordingFormat.Wav }, - }; - - public static Dictionary RecordingContentMap - { - get { return recContentMap; } - } - - public static Dictionary RecordingChannelMap - { - get { return recChannelMap; } - } - - public static Dictionary RecordingFormatMap - { - get { return recFormatMap; } - } - } - - public class AudioConfiguration - { - public int sampleRate { get; set; } - public int bitRate { get; set; } - public int channels { get; set; } - } - - public class VideoConfiguration - { - public int longerSideLength { get; set; } - public int shorterSideLength { get; set; } - public int framerate { get; set; } - public int bitRate { get; set; } - } - - public class RecordingInfo - { - public string contentType { get; set; } - public string channelType { get; set; } - public string format { get; set; } - public AudioConfiguration audioConfiguration { get; set; } - public VideoConfiguration videoConfiguration { get; set; } - } - - public class Participant - { - public string participantId { get; set; } - } - - public class Root - { - public string resourceId { get; set; } - public string callId { get; set; } - public string chunkDocumentId { get; set; } - public int chunkIndex { get; set; } - public DateTime chunkStartTime { get; set; } - public double chunkDuration { get; set; } - public List pauseResumeIntervals { get; set; } - public RecordingInfo recordingInfo { get; set; } - public List participants { get; set; } - } -} diff --git a/ServerRecording/Program.cs b/ServerRecording/Program.cs deleted file mode 100644 index 47ff8c23..00000000 --- a/ServerRecording/Program.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace QuickStartApi -{ - public class Program - { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); - } -} diff --git a/ServerRecording/README.MD b/ServerRecording/README.MD deleted file mode 100644 index 4ba1f9d9..00000000 --- a/ServerRecording/README.MD +++ /dev/null @@ -1,65 +0,0 @@ ---- -page_type: sample -languages: -- csharp -products: -- azure -- azure-communication-services ---- - -# Recording APIs Sample - -This is a sample application to show how the Azure Communication Services Call Automation SDK can be used to build a call recording feature. - -It's a C# based application powered by Dot net core to connect this application with Azure Communication Services. - -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/?WT.mc_id=A261C142F) -- [Visual Studio (2019 and above)](https://visualstudio.microsoft.com/vs/) -- [.NET Core 6.0](https://dotnet.microsoft.com/en-us/download/dotnet/6.0) (Make sure to install the 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 resource **connection string** for this quickstart. -- An Azure storage account and container, for details, see [Create a storage account](https://docs.microsoft.com/azure/storage/common/storage-account-create?tabs=azure-portal). You'll need to record your storage **connection string** and **container name** for this quickstart. -- Create a webhook and subscribe to the recording events. For details, see [Create webhook](https://docs.microsoft.com/azure/communication-services/quickstarts/voice-video-calling/download-recording-file-sample) - -## Code structure - -- ./ServerRecording/Controllers : Server app core logic for calling recording APIs using Azure Communication Services server calling SDK -- ./ServerRecording/Program.cs : Entry point for the server app program logic -- ./ServerRecording/Startup.cs : Entry point for the server app startup logic - -## 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-dotnet-quickstarts.git. -3. Once you get the config keys add the keys to the **Calling/appsetting.json** file found under the Calling folder. - - Input your ACS connection string in the variable: `ACSResourceConnectionString` - - Input your storage connection string in the variable: `BlobStorageConnectionString` - - Input blob container name for recorded media in the variable `BlobContainerName` - -## Locally deploying the sample app - -1. Go to Calling folder and open `RecordingApi.csproj` solution in Visual Studio -2. Run `RecordingApi` project. -3. Use postman or any debugging tool and open url - https://localhost:5001 - -### Troubleshooting - -1. Solution doesn\'t build, it throws errors during build - - Clean/rebuild the C# solution - -## Publish to Azure - -1. Right click the `RecordingApi` project and select Publish. -2. Create a new publish profile and select your app name, Azure subscription, resource group and etc. -3. Before publishing, add your connection string with `Edit App Service Settings`, and fill in `ResourceConnectionString` as key and connection string (copy from appsettings.json) as value -4. Detailed instructions on publishing the app to Azure are available at [Publish a Web app](https://docs.microsoft.com/visualstudio/deployment/quickstart-deploy-to-azure?view=vs-2019). - -**Note**: While you may use http://localhost for local testing, the sample when deployed will only work when served over https. The SDK [does not support http](https://docs.microsoft.com/azure/communication-services/concepts/voice-video-calling/calling-sdk-features#user-webrtc-over-https). - -## 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-6.0) - Framework for building web applications diff --git a/ServerRecording/Startup.cs b/ServerRecording/Startup.cs deleted file mode 100644 index ae8a1090..00000000 --- a/ServerRecording/Startup.cs +++ /dev/null @@ -1,56 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.HttpsPolicy; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace QuickStartApi -{ - public class Startup - { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - services.AddRazorPages(); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - else - { - app.UseExceptionHandler("/Error"); - // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. - app.UseHsts(); - } - - app.UseHttpsRedirection(); - app.UseStaticFiles(); - - app.UseRouting(); - - app.UseAuthorization(); - - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - } - } -}