Manage calls

Learn how to manage calls with the Azure Communication Services SDKS. We'll learn how to place calls, manage their participants and properties.

Prerequisites

Install the SDK

Use the npm install command to install the Azure Communication Services calling and common SDKs for JavaScript.

npm install @azure/communication-common --save
npm install @azure/communication-calling --save

Initialize required objects

A CallClient, instance is required for most call operations. Let's create a new CallClient instance. You can configure it with custom options like a Logger instance.

When you have a CallClient instance, you can create a CallAgent instance by calling the createCallAgent method on the CallClient instance. This method asynchronously returns a CallAgent instance object.

The createCallAgent method uses CommunicationTokenCredential as an argument. It accepts a user access token.

You can use the getDeviceManager method on the CallClient instance to access deviceManager.

const { CallClient } = require('@azure/communication-calling');
const { AzureCommunicationTokenCredential} = require('@azure/communication-common');
const { AzureLogger, setLogLevel } = require("@azure/logger");

// Set the logger's log level
setLogLevel('verbose');

// Redirect log output to wherever desired. To console, file, buffer, REST API, etc...
AzureLogger.log = (...args) => {
    console.log(...args); // Redirect log output to console
};

const userToken = '<USER_TOKEN>';
callClient = new CallClient(options);
const tokenCredential = new AzureCommunicationTokenCredential(userToken);
const callAgent = await callClient.createCallAgent(tokenCredential, {displayName: 'optional Azure Communication Services user name'});
const deviceManager = await callClient.getDeviceManager()

Place a call

To create and start a call, use one of the APIs on callAgent and provide a user that you've created through the Communication Services identity SDK.

Call creation and start are synchronous. The call instance allows you to subscribe to call events.

Place a 1:n call to a user or PSTN

To call another Communication Services user, use the startCall method on callAgent and pass the recipient's CommunicationUserIdentifier that you created with the Communication Services administration library.

For a 1:1 call to a user, use the following code:

const userCallee = { communicationUserId: '<ACS_USER_ID>' }
const oneToOneCall = callAgent.startCall([userCallee]);

To place a call to a public switched telephone network (PSTN), use the startCall method on callAgent and pass the recipient's PhoneNumberIdentifier. Your Communication Services resource must be configured to allow PSTN calling.

When you call a PSTN number, specify your alternate caller ID. An alternate caller ID is a phone number (based on the E.164 standard) that identifies the caller in a PSTN call. It's the phone number the call recipient sees for an incoming call.

Note

Please check details of PSTN calling offering. For preview program access, apply to the early adopter program.

For a 1:1 call to a PSTN number, use the following code:

const pstnCallee = { phoneNumber: '<ACS_USER_ID>' }
const alternateCallerId = {phoneNumber: '<ALTERNATE_CALLER_ID>'};
const oneToOneCall = callAgent.startCall([pstnCallee], {alternateCallerId});

For a 1:n call to a user and a PSTN number, use the following code:

const userCallee = { communicationUserId: '<ACS_USER_ID>' }
const pstnCallee = { phoneNumber: '<PHONE_NUMBER>'};
const alternateCallerId = {phoneNumber: '<ALTERNATE_CALLER_ID>'};
const groupCall = callAgent.startCall([userCallee, pstnCallee], {alternateCallerId});

Join a room call

To join a room call, you can instantiate a context object with the roomId property as the room identifier. To join the call, use the join method and pass the context instance.

const context = { roomId: '<RoomId>' }
const call = callAgent.join(context);

A room offers application developers better control over who can join a call, when they meet and how they collaborate. To learn more about rooms, you can read the conceptual documentation or follow the quick start guide.

Join a group call

Note

The groupId parameter is considered system metadata and may be used by Microsoft for operations that are required to run the system. Don't include personal data in the groupId value. Microsoft doesn't treat this parameter as personal data and its content may be visible to Microsoft employees or stored long-term.

The groupId parameter requires data to be in GUID format. We recommend using randomly generated GUIDs that aren't considered personal data in your systems.

To start a new group call or join an ongoing group call, use the join method and pass an object with a groupId property. The groupId value has to be a GUID.

const context = { groupId: '<GUID>'};
const call = callAgent.join(context);

Receive an incoming call

The callAgent instance emits an incomingCall event when the logged-in identity receives an incoming call. To listen to this event, subscribe by using one of these options:

const incomingCallHandler = async (args: { incomingCall: IncomingCall }) => {
    const incomingCall = args.incomingCall;	

    // Get incoming call ID
    var incomingCallId = incomingCall.id

    // Get information about this Call. This API is provided as a preview for developers
    // and may change based on feedback that we receive. Do not use this API in a production environment.
    // To use this api please use 'beta' release of Azure Communication Services Calling Web SDK
    var callInfo = incomingCall.info;

    // Get information about caller
    var callerInfo = incomingCall.callerInfo

    // Accept the call
    var call = await incomingCall.accept();

    // Reject the call
    incomingCall.reject();

    // Subscribe to callEnded event and get the call end reason
     incomingCall.on('callEnded', args => {
        console.log(args.callEndReason);
    });

    // callEndReason is also a property of IncomingCall
    var callEndReason = incomingCall.callEndReason;
};
callAgentInstance.on('incomingCall', incomingCallHandler);

The incomingCall event includes an incomingCall instance that you can accept or reject.

When starting/joining/accepting a call with video on, if the specified video camera device is being used by another process or if it is disabled in the system, the call will start with video off, and a cameraStartFailed: true call diagnostic will be raised.

Mute and unmute

To mute or unmute the local endpoint, you can use the mute and unmute asynchronous APIs:

//mute local device (microphone / sent audio)
await call.mute();

//unmute local device (microphone / sent audio)
await call.unmute();

Mute and unmute incoming audio

Mute incoming audio sets the call volume to 0. To mute or unmute the incoming audio, you can use the muteIncomingAudio and unmuteIncomingAudio asynchronous APIs:

//mute local device (speaker)
await call.muteIncomingAudio();

//unmute local device (speaker)
await call.unmuteIncomingAudio();

When incoming audio is muted, the participant will still receive the call audio (remote participant's audio). The call audio will not play in the speaker and the participant will not be able to listen until 'call.unmuteIncomingAudio()' is called. However, we can apply filter on call audio and play the filtered audio.

Manage remote participants

All remote participants are represented by RemoteParticipant type and available through remoteParticipants collection on a call instance.

List the participants in a call

The remoteParticipants collection returns a list of remote participants in a call:

call.remoteParticipants; // [remoteParticipant, remoteParticipant....]

Add a participant to a call

To add a participant (either a user or a phone number) to a call, you can use addParticipant. Provide one of the Identifier types. It synchronously returns the remoteParticipant instance. The remoteParticipantsUpdated event from Call is raised when a participant is successfully added to the call.

const userIdentifier = { communicationUserId: '<ACS_USER_ID>' };
const pstnIdentifier = { phoneNumber: '<PHONE_NUMBER>' }
const remoteParticipant = call.addParticipant(userIdentifier);
const remoteParticipant = call.addParticipant(pstnIdentifier, {alternateCallerId: '<ALTERNATE_CALLER_ID>'});

Remove a participant from a call

To remove a participant (either a user or a phone number) from a call, you can invoke removeParticipant. You have to pass one of the Identifier types. This method resolves asynchronously after the participant is removed from the call. The participant is also removed from the remoteParticipants collection.

const userIdentifier = { communicationUserId: '<ACS_USER_ID>' };
const pstnIdentifier = { phoneNumber: '<PHONE_NUMBER>' }
await call.removeParticipant(userIdentifier);
await call.removeParticipant(pstnIdentifier);

Access remote participant properties

Remote participants have a set of associated properties and collections:

  • CommunicationIdentifier: Get the identifier for a remote participant. Identity is one of the CommunicationIdentifier types:

    const identifier = remoteParticipant.identifier;
    

It can be one of the following CommunicationIdentifier types:

  • { communicationUserId: '<ACS_USER_ID'> }: Object representing the Azure Communication Services user.

  • { phoneNumber: '<E.164>' }: Object representing the phone number in E.164 format.

  • { microsoftTeamsUserId: '<TEAMS_USER_ID>', isAnonymous?: boolean; cloud?: "public" | "dod" | "gcch" }: Object representing the Teams user.

  • { id: string }: object representing identifier that doesn't fit any of the other identifier types

  • state: Get the state of a remote participant.

    const state = remoteParticipant.state;
    

The state can be:

  • Idle: Initial state.

  • Connecting: Transition state while a participant is connecting to the call.

  • Ringing: Participant is ringing.

  • Connected: Participant is connected to the call.

  • Hold: Participant is on hold.

  • EarlyMedia: Announcement that plays before a participant connects to the call.

  • InLobby: Indicates that remote participant is in lobby.

  • Disconnected: Final state. The participant is disconnected from the call. If the remote participant loses their network connectivity, their state changes to Disconnected after two minutes.

  • callEndReason: To learn why a participant left the call, check the callEndReason property:

    const callEndReason = remoteParticipant.callEndReason;
    const callEndReasonCode = callEndReason.code // (number) code associated with the reason
    const callEndReasonSubCode = callEndReason.subCode // (number) subCode associated with the reason
    

    Note:

    • This property is only set when adding a remote participant via the Call.addParticipant() API, and the remote participant declines for example.
    • In the scenario where for example, UserB kicks UserC, from UserA's perspective, UserA will not see this flag get set for UserC. In other words UserA will not see UserC's callEndReason property get set at all.
  • isMuted status: To find out if a remote participant is muted, check the isMuted property. It returns Boolean.

    const isMuted = remoteParticipant.isMuted;
    
  • isSpeaking status: To find out if a remote participant is speaking, check the isSpeaking property. It returns Boolean.

    const isSpeaking = remoteParticipant.isSpeaking;
    
  • videoStreams: To inspect all video streams that a given participant is sending in this call, check the videoStreams collection. It contains RemoteVideoStream objects.

    const videoStreams = remoteParticipant.videoStreams; // [RemoteVideoStream, ...]
    
  • displayName: To get display name for this remote participant, inspect displayName property it return string.

    const displayName = remoteParticipant.displayName;
    

Check call properties

Get the unique ID (string) for a call:

const callId: string = call.id;

Get information about the call:

Note

This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. To use this api please use 'beta' release of Azure Communication Services Calling Web SDK

const callInfo = call.info;

Learn about other participants in the call by inspecting the remoteParticipants collection on the 'call' instance:

const remoteParticipants = call.remoteParticipants;

Identify the caller of an incoming call:

const callerIdentity = call.callerInfo.identifier;

identifier is one of the CommunicationIdentifier types.

Get the state of a call:

const callState = call.state;

This returns a string representing the current state of a call:

  • None: Initial call state.
  • Connecting: Initial transition state when a call is placed or accepted.
  • Ringing: For an outgoing call, indicates that a call is ringing for remote participants. It is Incoming on their side.
  • EarlyMedia: Indicates a state in which an announcement is played before the call is connected.
  • Connected: Indicates that the call is connected.
  • LocalHold: Indicates that the call is put on hold by a local participant. No media is flowing between the local endpoint and remote participants.
  • RemoteHold: Indicates that the call was put on hold by remote participant. No media is flowing between the local endpoint and remote participants.
  • InLobby: Indicates that user is in lobby.
  • Disconnecting: Transition state before the call goes to a Disconnected state.
  • Disconnected: Final call state. If the network connection is lost, the state changes to Disconnected after two minutes.

Find out why a call ended by inspecting the callEndReason property:

const callEndReason = call.callEndReason;
const callEndReasonCode = callEndReason.code // (number) code associated with the reason
const callEndReasonSubCode = callEndReason.subCode // (number) subCode associated with the reason

Learn if the current call is incoming or outgoing by inspecting the direction property. It returns CallDirection.

const isIncoming = call.direction == 'Incoming';
const isOutgoing = call.direction == 'Outgoing';

Check if the current microphone is muted. It returns Boolean.

const muted = call.isMuted;

Check if the current incoming audio (speaker) is muted. It returns Boolean.

const incomingAudioMuted = call._isIncomingAudioMuted;

Find out if the screen sharing stream is being sent from a given endpoint by checking the isScreenSharingOn property. It returns Boolean.

const isScreenSharingOn = call.isScreenSharingOn;

Inspect active video streams by checking the localVideoStreams collection. It returns LocalVideoStream objects.

const localVideoStreams = call.localVideoStreams;

Install the SDK

Locate your project level build.gradle and make sure to add mavenCentral() to the list of repositories under buildscript and allprojects

buildscript {
    repositories {
    ...
        mavenCentral()
    ...
    }
}
allprojects {
    repositories {
    ...
        mavenCentral()
    ...
    }
}

Then, in your module level build.gradle add the following lines to the dependencies section

dependencies {
    ...
    implementation 'com.azure.android:azure-communication-calling:1.0.0'
    ...
}

Initialize the required objects

To create a CallAgent instance you have to call the createCallAgent method on a CallClient instance. This asynchronously returns a CallAgent instance object. The createCallAgent method takes a CommunicationUserCredential as an argument, which encapsulates an access token. To access the DeviceManager, a callAgent instance must be created first, and then you can use the CallClient.getDeviceManager method to get the DeviceManager.

String userToken = '<user token>';
CallClient callClient = new CallClient();
CommunicationTokenCredential tokenCredential = new CommunicationTokenCredential(userToken);
android.content.Context appContext = this.getApplicationContext(); // From within an Activity for instance
CallAgent callAgent = callClient.createCallAgent(appContext, tokenCredential).get();
DeviceManager deviceManager = callClient.getDeviceManager(appContext).get();

To set a display name for the caller, use this alternative method:

String userToken = '<user token>';
CallClient callClient = new CallClient();
CommunicationTokenCredential tokenCredential = new CommunicationTokenCredential(userToken);
android.content.Context appContext = this.getApplicationContext(); // From within an Activity for instance
CallAgentOptions callAgentOptions = new CallAgentOptions();
callAgentOptions.setDisplayName("Alice Bob");
DeviceManager deviceManager = callClient.getDeviceManager(appContext).get();
CallAgent callAgent = callClient.createCallAgent(appContext, tokenCredential, callAgentOptions).get();

Place a call

To create and start a call you need to call the CallAgent.startCall() method and provide the Identifier of the callee(s). To join a group call you need to call the CallAgent.join() method and provide the groupId. Group Ids must be in GUID or UUID format.

Call creation and start are synchronous. The call instance allows you to subscribe to all events on the call.

Place a 1:1 call to a user

To place a call to another Communication Services user, invoke the call method on callAgent and pass an object with communicationUserId key.

StartCallOptions startCallOptions = new StartCallOptions();
Context appContext = this.getApplicationContext();
CommunicationUserIdentifier acsUserId = new CommunicationUserIdentifier(<USER_ID>);
CommunicationUserIdentifier participants[] = new CommunicationUserIdentifier[]{ acsUserId };
call oneToOneCall = callAgent.startCall(appContext, participants, startCallOptions);

Place a 1:n call with users and PSTN

Note

Please check details of PSTN calling offering. For preview program access, apply to the early adopter program.

To place a 1:n call to a user and a PSTN number you have to specify the phone number of callee. Your Communication Services resource must be configured to allow PSTN calling:

CommunicationUserIdentifier acsUser1 = new CommunicationUserIdentifier(<USER_ID>);
PhoneNumberIdentifier acsUser2 = new PhoneNumberIdentifier("<PHONE_NUMBER>");
CommunicationIdentifier participants[] = new CommunicationIdentifier[]{ acsUser1, acsUser2 };
StartCallOptions startCallOptions = new StartCallOptions();
Context appContext = this.getApplicationContext();
Call groupCall = callAgent.startCall(participants, startCallOptions);

Accept a call

To accept a call, call the 'accept' method on a call object.

Context appContext = this.getApplicationContext();
IncomingCall incomingCall = retrieveIncomingCall();
Call call = incomingCall.accept(context).get();

To accept a call with video camera on:

Context appContext = this.getApplicationContext();
IncomingCall incomingCall = retrieveIncomingCall();
AcceptCallOptions acceptCallOptions = new AcceptCallOptions();
VideoDeviceInfo desiredCamera = callClient.getDeviceManager().get().getCameraList().get(0);
acceptCallOptions.setVideoOptions(new VideoOptions(new LocalVideoStream(desiredCamera, appContext)));
Call call = incomingCall.accept(context, acceptCallOptions).get();

The incoming call can be obtained by subscribing to the onIncomingCall event on the callAgent object:

// Assuming "callAgent" is an instance property obtained by calling the 'createCallAgent' method on CallClient instance 
public Call retrieveIncomingCall() {
    IncomingCall incomingCall;
    callAgent.addOnIncomingCallListener(new IncomingCallListener() {
        void onIncomingCall(IncomingCall inboundCall) {
            // Look for incoming call
            incomingCall = inboundCall;
        }
    });
    return incomingCall;
}

Join a room call

Use the CallAgent and RoomCallLocator to join a room call by specifying a roomId. The CallAgent.join method will return a Call object:

val roomCallLocator = RoomCallLocator(roomId)
call = callAgent.join(applicationContext, roomCallLocator, joinCallOptions)

A room offers application developers better control over who can join a call, when they meet and how they collaborate. To learn more about rooms, you can read the conceptual documentation or follow the quick start guide.

Join a group call

To start a new group call or join an ongoing group call you have to call the 'join' method and pass an object with a groupId property. The value has to be a GUID.

Context appContext = this.getApplicationContext();
GroupCallLocator groupCallLocator = new GroupCallLocator("<GUID>");
JoinCallOptions joinCallOptions = new JoinCallOptions();

call = callAgent.join(context, groupCallLocator, joinCallOptions);

Call properties

Get the unique ID for this Call:

String callId = call.getId();

To learn about other participants in the call inspect remoteParticipant collection on the call instance:

List<RemoteParticipant> remoteParticipants = call.getRemoteParticipants();

The identity of caller if the call is incoming:

CommunicationIdentifier callerId = call.getCallerInfo().getIdentifier();

Get the state of the Call:

CallState callState = call.getState();

It returns a string representing the current state of a call:

  • 'NONE' - initial call state
  • 'EARLY_MEDIA' - indicates a state in which an announcement is played before call is connected
  • 'CONNECTING' - initial transition state once call is placed or accepted
  • 'RINGING' - for an outgoing call - indicates call is ringing for remote participants
  • 'CONNECTED' - call is connected
  • 'LOCAL_HOLD' - call is put on hold by local participant, no media is flowing between local endpoint and remote participant(s)
  • 'REMOTE_HOLD' - call is put on hold by a remote participant, no media is flowing between local endpoint and remote participant(s)
  • 'DISCONNECTING' - transition state before call goes to 'Disconnected' state
  • 'DISCONNECTED' - final call state
  • 'IN_LOBBY' - in lobby for a Teams meeting interoperability

To learn why a call ended, inspect callEndReason property. It contains code/subcode:

CallEndReason callEndReason = call.getCallEndReason();
int code = callEndReason.getCode();
int subCode = callEndReason.getSubCode();

To see if the current call is an incoming or an outgoing call, inspect callDirection property:

CallDirection callDirection = call.getCallDirection(); 
// callDirection == CallDirection.INCOMING for incoming call
// callDirection == CallDirection.OUTGOING for outgoing call

To see if the current microphone is muted, inspect the muted property:

boolean muted = call.isMuted();

To inspect active video streams, check the localVideoStreams collection:

List<LocalVideoStream> localVideoStreams = call.getLocalVideoStreams();

Mute and unmute

To mute or unmute the local endpoint you can use the mute and unmute asynchronous APIs:

Context appContext = this.getApplicationContext();
call.mute(appContext).get();
call.unmute(appContext).get();

Change the volume of the call

While you are in a call, the hardware volume keys on the phone should allow the user to change the call volume. This is done by using the method setVolumeControlStream with the stream type AudioManager.STREAM_VOICE_CALL on the Activity where the call is being placed. This allows the hardware volume keys to change the volume of the call (denoted by a phone icon or something similar on the volume slider), preventing to change the volume for other sound profiles, like alarms, media or system wide volume. For more information, you can check Handling changes in audio output | Android Developers.

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    setVolumeControlStream(AudioManager.STREAM_VOICE_CALL);
}

Remote participants management

All remote participants are represented by RemoteParticipant type and are available through the remoteParticipants collection on a call instance.

List participants in a call

The remoteParticipants collection returns a list of remote participants in given call:

List<RemoteParticipant> remoteParticipants = call.getRemoteParticipants(); // [remoteParticipant, remoteParticipant....]

Add a participant to a call

To add a participant to a call (either a user or a phone number) you can invoke addParticipant. This will synchronously return the remote participant instance.

const acsUser = new CommunicationUserIdentifier("<acs user id>");
const acsPhone = new PhoneNumberIdentifier("<phone number>");
RemoteParticipant remoteParticipant1 = call.addParticipant(acsUser);
AddPhoneNumberOptions addPhoneNumberOptions = new AddPhoneNumberOptions(new PhoneNumberIdentifier("<alternate phone number>"));
RemoteParticipant remoteParticipant2 = call.addParticipant(acsPhone, addPhoneNumberOptions);

Remove participant from a call

To remove a participant from a call (either a user or a phone number) you can invoke removeParticipant. This will resolve asynchronously once the participant is removed from the call. The participant will also be removed from remoteParticipants collection.

RemoteParticipant acsUserRemoteParticipant = call.getParticipants().get(0);
RemoteParticipant acsPhoneRemoteParticipant = call.getParticipants().get(1);
call.removeParticipant(acsUserRemoteParticipant).get();
call.removeParticipant(acsPhoneRemoteParticipant).get();

Remote participant properties

Any given remote participant has a set of properties and collections associated with it:

  • Get the identifier for this remote participant. Identity is one of the 'Identifier' types

    CommunicationIdentifier participantIdentifier = remoteParticipant.getIdentifier();
    
  • Get state of this remote participant.

    ParticipantState state = remoteParticipant.getState();
    

State can be one of

  • 'IDLE' - initial state

  • 'EARLY_MEDIA' - announcement is played before participant is connected to the call

  • 'RINGING' - participant call is ringing

  • 'CONNECTING' - transition state while participant is connecting to the call

  • 'CONNECTED' - participant is connected to the call

  • 'HOLD' - participant is on hold

  • 'IN_LOBBY' - participant is waiting in the lobby to be admitted. Currently only used in Teams interop scenario

  • 'DISCONNECTED' - final state - participant is disconnected from the call

  • To learn why a participant left the call, inspect callEndReason property:

    CallEndReason callEndReason = remoteParticipant.getCallEndReason();
    
  • To check whether this remote participant is muted or not, inspect the isMuted property:

    boolean isParticipantMuted = remoteParticipant.isMuted();
    
  • To check whether this remote participant is speaking or not, inspect the isSpeaking property:

    boolean isParticipantSpeaking = remoteParticipant.isSpeaking();
    
  • To inspect all video streams that a given participant is sending in this call, check the videoStreams collection:

    List<RemoteVideoStream> videoStreams = remoteParticipant.getVideoStreams(); // [RemoteVideoStream, RemoteVideoStream, ...]
    

Using Foreground Services

In cases when you want to run a user visible task even when your application is in background, you can use Foreground Services.

Using Foreground Services, you can for example, keeps a user visible notification when your application has an active call. This way, even if the user goes to the homescreen or removes the application from the recent's screen, the call will continue to be active.

If you don't use a Foreground Service while in a call, navigating to the homescreen can keep the call active, but removing the application from the recent's screen can stop the call if the Android OS kills your application's process.

You should start the Foreground Service when you start/join a call, for example:

call = callAgent.startCall(context, participants, options);
startService(yourForegroundServiceIntent);

And stop the Foreground Service when you hangup the call or the call's status is Disconnected, for example:

call.hangUp(new HangUpOptions()).get();
stopService(yourForegroundServiceIntent);

Notes on using Foreground Services

Keep in mind that scenarios like stopping an already running Foreground Service when the app is removed from the recent's list, will remove the user visible notification and the Android OS can keep your application process alive for some extra period of time, meaning that the call can still be active during this period.

If your application is stopping the Foreground Service on the service onTaskRemoved method for example, your application can start/stop audio and video according to your Activity Lifecycle like stopping audio and video when your activity is destroyed with the onDestroy method override.

Set up your system

Create the Xcode project

In Xcode, create a new iOS project and select the Single View App template. This quickstart uses the SwiftUI framework, so you should set the Language to Swift and User Interface to SwiftUI.

You're not going to create unit tests or UI tests during this quickstart. Feel free to clear the Include Unit Tests and Include UI Tests text boxes.

Screenshot that shows the window for creating a project within Xcode.

Install the package and dependencies with CocoaPods

  1. Create a Podfile for your application, like this:

    platform :ios, '13.0'
    use_frameworks!
    target 'AzureCommunicationCallingSample' do
        pod 'AzureCommunicationCalling', '~> 1.0.0'
    end
    
  2. Run pod install.

  3. Open .xcworkspace with Xcode.

Request access to the microphone

To access the device's microphone, you need to update your app's information property list with NSMicrophoneUsageDescription. You set the associated value to a string that will be included in the dialog that the system uses to request access from the user.

Right-click the Info.plist entry of the project tree and select Open As > Source Code. Add the following lines in the top-level <dict> section, and then save the file.

<key>NSMicrophoneUsageDescription</key>
<string>Need microphone access for VOIP calling.</string>

Set up the app framework

Open your project's ContentView.swift file and add an import declaration to the top of the file to import the AzureCommunicationCalling library. In addition, import AVFoundation. You'll need it for audio permission requests in the code.

import AzureCommunicationCalling
import AVFoundation

Initialize CallAgent

To create a CallAgent instance from CallClient, you have to use a callClient.createCallAgent method that asynchronously returns a CallAgent object after it's initialized.

To create a call client, you have to pass a CommunicationTokenCredential object.

import AzureCommunication

let tokenString = "token_string"
var userCredential: CommunicationTokenCredential?
do {
    let options = CommunicationTokenRefreshOptions(initialToken: token, refreshProactively: true, tokenRefresher: self.fetchTokenSync)
    userCredential = try CommunicationTokenCredential(withOptions: options)
} catch {
    updates("Couldn't created Credential object", false)
    initializationDispatchGroup!.leave()
    return
}

// tokenProvider needs to be implemented by Contoso, which fetches a new token
public func fetchTokenSync(then onCompletion: TokenRefreshOnCompletion) {
    let newToken = self.tokenProvider!.fetchNewToken()
    onCompletion(newToken, nil)
}

Pass the CommunicationTokenCredential object that you created to CallClient, and set the display name.

self.callClient = CallClient()
let callAgentOptions = CallAgentOptions()
options.displayName = " iOS Azure Communication Services User"

self.callClient!.createCallAgent(userCredential: userCredential!,
    options: callAgentOptions) { (callAgent, error) in
        if error == nil {
            print("Create agent succeeded")
            self.callAgent = callAgent
        } else {
            print("Create agent failed")
        }
})

Note

When the application implements event delegates, it has to hold a strong reference to the objects that require event subscriptions. For example, when a RemoteParticipant object is returned on invoking the call.addParticipant method and the application sets the delegate to listen on RemoteParticipantDelegate, the application must hold a strong reference to the RemoteParticipant object. Otherwise, if this object gets collected, the delegate will throw a fatal exception when the Calling SDK tries to invoke the object.

Place an outgoing call

To create and start a call, you need to call one of the APIs on CallAgent and provide the Communication Services identity of a user that you've provisioned by using the Communication Services Management SDK.

Call creation and start are synchronous. You'll receive a call instance that allows you to subscribe to all events on the call.

Place a 1:1 call to a user or a 1:n call with users and PSTN

let callees = [CommunicationUser(identifier: 'UserId')]
self.callAgent?.startCall(participants: callees, options: StartCallOptions()) { (call, error) in
     if error == nil {
         print("Successfully started outgoing call")
         self.call = call
     } else {
         print("Failed to start outgoing call")
     }
}

Place a 1:n call with users and PSTN

Note

Please check details of PSTN calling offering. For preview program access, apply to the early adopter program.

To place the call to PSTN, you have to specify a phone number acquired with Communication Services.

let pstnCallee = PhoneNumberIdentifier(phoneNumber: '+1999999999')
let callee = CommunicationUserIdentifier('UserId')
self.callAgent?.startCall(participants: [pstnCallee, callee], options: StartCallOptions()) { (groupCall, error) in
     if error == nil {
         print("Successfully started outgoing call to multiple participants")
         self.call = groupCall
     } else {
         print("Failed to start outgoing call to multiple participants")
     }
}

Join a room call

To join a room call, specify the roomId property as the room identifier. To join the call, use the join method and pass the roomCallLocator.

func joinRoomCall() {
    if self.callAgent == nil {
        print("CallAgent not initialized")
        return
    }
    
    if (self.roomId.isEmpty) {
        print("Room ID not set")
        return
    }
    
    // Join a call with a Room ID
    let options = JoinCallOptions()
    let audioOptions = AudioOptions()
    audioOptions.muted = self.muted
    
    options.audioOptions = audioOptions
    
    let roomCallLocator = RoomCallLocator(roomId: roomId)
    self.callAgent!.join(with: roomCallLocator, joinCallOptions: options) { (call, error) in
        self.setCallAndObserver(call: call, error: error)
    }
}

A room offers application developers better control over who can join a call, when they meet and how they collaborate. To learn more about rooms, you can read the conceptual documentation or follow the quick start guide.

Join a group call

To join a call, you need to call one of the APIs on CallAgent.

let groupCallLocator = GroupCallLocator(groupId: UUID(uuidString: "uuid_string")!)
self.callAgent?.join(with: groupCallLocator, joinCallOptions: JoinCallOptions()) { (call, error) in
    if error == nil {
        print("Successfully joined group call")
        self.call = call
    } else {
        print("Failed to join group call")
    }
}

Subscribe to an incoming call

Subscribe to an incoming call event.

final class IncomingCallHandler: NSObject, CallAgentDelegate, IncomingCallDelegate
{
    // Event raised when there is an incoming call
    public func callAgent(_ callAgent: CallAgent, didReceiveIncomingCall incomingcall: IncomingCall) {
        self.incomingCall = incomingcall
        // Subscribe to get OnCallEnded event
        self.incomingCall?.delegate = self
    }

    // Event raised when incoming call was not answered
    public func incomingCall(_ incomingCall: IncomingCall, didEnd args: PropertyChangedEventArgs) {
        print("Incoming call was not answered")
        self.incomingCall = nil
    }
}

Accept an incoming call

To accept a call, call the accept method on a IncomingCall object.

self.incomingCall!.accept(options: AcceptCallOptions()) { (call, error) in
   if (error == nil) {
       print("Successfully accepted incoming call")
       self.call = call
   } else {
       print("Failed to accept incoming call")
   }
}

let firstCamera: VideoDeviceInfo? = self.deviceManager!.cameras.first
localVideoStreams = [LocalVideoStream]()
localVideoStreams!.append(LocalVideoStream(camera: firstCamera!))
let acceptCallOptions = AcceptCallOptions()
acceptCallOptions.videoOptions = VideoOptions(localVideoStreams: localVideoStreams!)
if let incomingCall = self.incomingCall {
    incomingCall.accept(options: acceptCallOptions) { (call, error) in
        if error == nil {
            print("Incoming call accepted")
        } else {
            print("Failed to accept incoming call")
        }
    }
} else {
  print("No incoming call found to accept")
}

Perform mid-call operations

You can perform various operations during a call to manage settings related to video and audio.

Mute and unmute

To mute or unmute the local endpoint, you can use the mute and unmute asynchronous APIs.

call!.mute { (error) in
    if error == nil {
        print("Successfully muted")
    } else {
        print("Failed to mute")
    }
}

Use the following code to unmute the local endpoint asynchronously.

call!.unmute { (error) in
    if error == nil {
        print("Successfully un-muted")
    } else {
        print("Failed to unmute")
    }
}

Manage remote participants

All remote participants are represented by the RemoteParticipant type and are available through the remoteParticipants collection on a call instance.

List participants in a call

call.remoteParticipants

Add a participant to a call

To add a participant to a call (either a user or a phone number), you can invoke addParticipant. This command will synchronously return a remote participant instance.

let remoteParticipantAdded: RemoteParticipant = call.add(participant: CommunicationUserIdentifier(identifier: "userId"))

Remove a participant from a call

To remove a participant from a call (either a user or a phone number), you can invoke the removeParticipant API. This will resolve asynchronously.

call!.remove(participant: remoteParticipantAdded) { (error) in
    if (error == nil) {
        print("Successfully removed participant")
    } else {
        print("Failed to remove participant")
    }
}

Get remote participant properties

// [RemoteParticipantDelegate] delegate - an object you provide to receive events from this RemoteParticipant instance
var remoteParticipantDelegate = remoteParticipant.delegate

// [CommunicationIdentifier] identity - same as the one used to provision a token for another user
var identity = remoteParticipant.identifier

// ParticipantStateIdle = 0, ParticipantStateEarlyMedia = 1, ParticipantStateConnecting = 2, ParticipantStateConnected = 3, ParticipantStateOnHold = 4, ParticipantStateInLobby = 5, ParticipantStateDisconnected = 6
var state = remoteParticipant.state

// [Error] callEndReason - reason why participant left the call, contains code/subcode/message
var callEndReason = remoteParticipant.callEndReason

// [Bool] isMuted - indicating if participant is muted
var isMuted = remoteParticipant.isMuted

// [Bool] isSpeaking - indicating if participant is currently speaking
var isSpeaking = remoteParticipant.isSpeaking

// RemoteVideoStream[] - collection of video streams this participants has
var videoStreams = remoteParticipant.videoStreams // [RemoteVideoStream, RemoteVideoStream, ...]

Setting up

Creating the Visual Studio project

For UWP app, in Visual Studio 2019, create a new Blank App (Universal Windows) project. After entering the project name, feel free to pick any Windows SDK greater than 10.0.17134.

For WinUI 3 app, create a new project with the Blank App, Packaged (WinUI 3 in Desktop) template to set up a single-page WinUI 3 app. Windows App SDK version 1.2 preview 2 and above is required.

Install the package and dependencies with NuGet Package Manager

The Calling SDK APIs and libraries are publicly available via a NuGet package. The following steps exemplify how to find, download, and install the Calling SDK NuGet package.

  1. Open NuGet Package Manager (Tools -> NuGet Package Manager -> Manage NuGet Packages for Solution)
  2. Click on Browse and then type Azure.Communication.Calling in the search box.
  3. Make sure that Include prerelease check box is selected.
  4. Click on the Azure.Communication.Calling package, select Azure.Communication.Calling 1.0.0-beta.33 or newer version.
  5. Select the checkbox corresponding to the CS project on the right-side tab.
  6. Click on the Install button.

Request access to the microphone

The app will require access to the microphone to run properly. In UWP apps, the microphone capability should be declared in the app manifest file. he following steps exemplify how to achieve that.

  1. In the Solution Explorer panel, double click on the file with .appxmanifest extension.
  2. Click on the Capabilities tab.
  3. Select the Microphone check box from the capabilities list.

Create UI buttons to place and hang up the call

This simple sample app will contain two buttons. One for placing the call and another to hang up a placed call. The following steps exemplify how to add these buttons to the app.

  1. In the Solution Explorer panel, double click on the file named MainPage.xaml for UWP, or MainWindows.xaml for WinUI 3.
  2. In the central panel, look for the XAML code under the UI preview.
  3. Modify the XAML code by the following excerpt:
<TextBox x:Name="CalleeTextBox" Text="Who would you like to call?" TextWrapping="Wrap" VerticalAlignment="Center" Grid.Row="0" Height="40" Margin="10,10,10,10" />
<StackPanel Orientation="Horizontal">
    <Button x:Name="CallButton" Content="Start Call" Click="CallButton_Click" VerticalAlignment="Center" Margin="10,0,0,0" Height="40" Width="200"/>
    <Button x:Name="HangupButton" Content="Hang Up" Click="HangupButton_Click" VerticalAlignment="Center" Margin="10,0,0,0" Height="40" Width="200"/>
</StackPanel>

Setting up the app with Calling SDK APIs

The Calling SDK APIs are in two different namespaces. The following steps inform the C# compiler about these namespaces allowing Visual Studio's Intellisense to assist with code development.

  1. In the Solution Explorer panel, click on the arrow on the left side of the file named MainPage.xaml for UWP, or MainWindows.xaml for WinUI 3.
  2. Double click on file named MainPage.xaml.cs or MainWindows.xaml.cs.
  3. Add the following commands at the bottom of the current using statements.
using Azure.Communication;
using Azure.Communication.Calling;

Please keep MainPage.xaml.cs or MainWindows.xaml.cs open. The next steps will add more code to it.

Allow app interactions

The UI buttons previously added need to operate on top of a placed Call. It means that a Call data member should be added to the MainPage or MainWindow class. Additionally, to allow the asynchronous operation creating CallAgent to succeed, a CallAgent data member should also be added to the same class.

Please add the following data members to the MainPage pr MainWindow class:

CallAgent callAgent;
Call call;

Create button handlers

Previously, two UI buttons were added to the XAML code. The following code adds the handlers to be executed when a user selects the button. The following code should be added after the data members from the previous section.

private async void CallButton_Click(object sender, RoutedEventArgs e)
{
    // Start call
}

private async void HangupButton_Click(object sender, RoutedEventArgs e)
{
    // End the current call
}

Object model

The following classes and interfaces handle some of the major features of the Azure Communication Services Calling client library for UWP.

Name Description
CallClient The CallClient is the main entry point to the Calling client library.
CallAgent The CallAgent is used to start and join calls.
Call The Call is used to manage placed or joined calls.
CommunicationTokenCredential The CommunicationTokenCredential is used as the token credential to instantiate the CallAgent.
CallAgentOptions The CallAgentOptions contains information to identify the caller.
HangupOptions The HangupOptions informs if a call should be terminated to all its participants.

Initialize the CallAgent

To create a CallAgent instance from CallClient you must use CallClient.CreateCallAgent method that asynchronously returns a CallAgent object once it is initialized.

To create CallAgent, you must pass a CommunicationTokenCredential object and a CallAgentOptions object. Keep in mind that CommunicationTokenCredential throws if a malformed token is passed.

The following code should be added inside and helper function to be called in app initialization.

var callClient = new CallClient();
this.deviceManager = await callClient.GetDeviceManager();

var tokenCredential = new CommunicationTokenCredential("<AUTHENTICATION_TOKEN>");
var callAgentOptions = new CallAgentOptions()
{
    DisplayName = "<DISPLAY_NAME>"
};

this.callAgent = await callClient.CreateCallAgent(tokenCredential, callAgentOptions);

<AUTHENTICATION_TOKEN> must be replaced by a valid credential token for your resource. Refer to the user access token documentation if a credential token has to be sourced.

Create CallAgent and place a call

The objects needed for creating a CallAgent are now ready. It is time to asynchronously create CallAgent and place a call.

The following code should be added after handling the exception from the previous step.

var startCallOptions = new StartCallOptions();

var callees = new ICommunicationIdentifier[1] { new CommunicationUserIdentifier(CalleeTextBox.Text.Trim()) };

this.call = await this.callAgent.StartCallAsync(callees, startCallOptions);
this.call.OnStateChanged += Call_OnStateChangedAsync;

Feel free to use 8:echo123 to talk to the Azure Communication Services echo bot.

End a call

Once a call is placed, the HangupAsync method of the Call object should be used to hang up the call.

An instance of HangupOptions should also be used to inform if the call must be terminated to all its participants.

The following code should be added inside HangupButton_Click.

this.call.OnStateChanged -= Call_OnStateChangedAsync;
await this.call.HangUpAsync(new HangUpOptions());

Run the code

Make sure Visual Studio will build the app for x64, x86 or ARM64, then hit F5 to start running the app. After that, click on the Call button to place a call to the callee defined.

Keep in mind that the first time the app runs, the system will prompt user for granting access to the microphone.

Next steps