Quickstart: Use the Communication Services Calling SDK

Get started with Azure Communication Services by using the Communication Services Calling SDK to add voice and video calling to your app.

Prerequisites

Install the SDK

Note

This document uses ACS Calling Web 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

Object model

The following classes and interfaces handle some of the major features of the Azure Communication Services Calling SDK:

Name Description
CallClient The main entry point to the Calling SDK.
CallAgent Used to start and manage calls.
DeviceManager Used to manage media devices.
AzureCommunicationTokenCredential Implements the CommunicationTokenCredential interface, which is used to instantiate callAgent.

Initialize a CallClient instance, create a CallAgent instance, and access deviceManager

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 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.

// Set the logger's log level
setLogLevel('verbose');
// Redirect logger output to wherever desired. By default it logs to console
AzureLogger.log = (...args) => { console.log(...args) };
const userToken = '<user token>';
callClient = new CallClient(options);
const tokenCredential = new AzureCommunicationTokenCredential(userToken);
const callAgent = await callClient.createCallAgent(tokenCredential, {displayName: 'optional ACS 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.

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

PSTN calling is currently in private preview. For access, apply to the early adopter program.

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

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

For a 1:n call, use the following code:

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

Place a 1:1 call with video camera

Important

There can currently be no more than one outgoing local video stream.

To place a video call, you have to enumerate local cameras by using the getCameras() method in deviceManager.

After you select a camera, use it to construct a LocalVideoStream instance. Pass it within videoOptions as an item within the localVideoStream array to the startCall method.

const deviceManager = await callClient.getDeviceManager();
const cameras = await deviceManager.getCameras();
const camera = cameras[0]
localVideoStream = new LocalVideoStream(camera);
const placeCallOptions = {videoOptions: {localVideoStreams:[localVideoStream]}};
const call = callAgent.startCall(['acsUserId'], placeCallOptions);

When your call connects, it automatically starts sending a video stream from the selected camera to the other participant. This also applies to the Call.Accept() video options and CallAgent.join() video options.

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);

Join a Teams Meeting

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 ACS Calling Web SDK

To join a Teams meeting, use the join method and pass a meeting link or a meeting's coordinates.

Join by using a meeting link:

const locator = { meetingLink: <meeting link>}
const call = callAgent.join(locator);

Join by using meeting coordinates:

const locator = {
    threadId: <thread id>,
    organizerId: <organizer id>,
    tenantId: <tenant id>,
    messageId: <message id>
}
const call = callAgent.join(locator);

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 incomingCallHander = 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 ACS 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', incomingCallHander);

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

Manage calls

During a call, you can access call properties and manage video and audio settings.

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 ACS 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's 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;

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;

Mute and unmute

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


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

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

Start and stop sending local video

To start a video, you have to enumerate cameras using the getCameras method on the deviceManager object. Then create a new instance of LocalVideoStream with the desired camera and then pass the LocalVideoStream object into the startVideo method:

const deviceManager = await callClient.getDeviceManager();
const cameras = await deviceManager.getCameras();
const camera = cameras[0]
const localVideoStream = new LocalVideoStream(camera);
await call.startVideo(localVideoStream);

After you successfully start sending video, a LocalVideoStream instance is added to the localVideoStreams collection on a call instance.

call.localVideoStreams[0] === localVideoStream;

To stop local video, pass the localVideoStream instance that's available in the localVideoStreams collection:

await call.stopVideo(localVideoStream);

You can switch to a different camera device while a video is sending by invoking switchSource on a localVideoStream instance:

const cameras = await callClient.getDeviceManager().getCameras();
const camera = cameras[1];
localVideoStream.switchSource(camera);

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....]

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 ACS 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 repredenting 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
    
  • 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;
    

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 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);

Render remote participant video streams

To list the video streams and screen sharing streams of remote participants, inspect the videoStreams collections:

const remoteVideoStream: RemoteVideoStream = call.remoteParticipants[0].videoStreams[0];
const streamType: MediaStreamType = remoteVideoStream.mediaStreamType;

To render RemoteVideoStream, you have to subscribe to it's isAvailableChanged event. If the isAvailable property changes to true, a remote participant is sending a stream. After that happens, create a new instance of VideoStreamRenderer, and then create a new VideoStreamRendererView instance by using the asynchronous createView method. You can then attach view.target to any UI element.

Whenever availability of a remote stream changes you can choose to destroy the whole VideoStreamRenderer, a specific VideoStreamRendererView or keep them, but this will result in displaying blank video frame.

function subscribeToRemoteVideoStream(remoteVideoStream: RemoteVideoStream) {
	let videoStreamRenderer: VideoStreamRenderer = new VideoStreamRenderer(remoteVideoStream);
	const displayVideo = () => {
		const view = await videoStreamRenderer.createView();
		htmlElement.appendChild(view.target);
	}
	remoteVideoStream.on('isAvailableChanged', async () => {
		if (remoteVideoStream.isAvailable) {
			displayVideo();
		} else {
			videoStreamRenderer.dispose();
		}
	});
	if (remoteVideoStream.isAvailable) {
		displayVideo();
	}
}

Remote video stream properties

Remote video streams have the following properties:

  • id: The ID of a remote video stream.

    const id: number = remoteVideoStream.id;
    
  • mediaStreamType: Can be Video or ScreenSharing.

    const type: MediaStreamType = remoteVideoStream.mediaStreamType;
    
  • isAvailable: Whether a remote participant endpoint is actively sending a stream.

    const type: boolean = remoteVideoStream.isAvailable;
    

VideoStreamRenderer methods and properties

Create a VideoStreamRendererView instance that can be attached in the application UI to render the remote video stream, use asynchronous createView() method, it resolves when stream is ready to render and returns an object with target property that represents video element that can be appended anywhere in the DOM tree

videoStreamRenderer.createView()

Dispose of videoStreamRenderer and all associated VideoStreamRendererView instances:

videoStreamRenderer.dispose()

VideoStreamRendererView methods and properties

When you create a VideoStreamRendererView, you can specify the scalingMode and isMirrored properties. scalingMode can be Stretch, Crop, or Fit. If isMirrored is specified, the rendered stream is flipped vertically.

const videoStreamRendererView: VideoStreamRendererView = await videoStreamRenderer.createView({ scalingMode, isMirrored });

Every VideoStreamRendererView instance has a target property that represents the rendering surface. Attach this property in the application UI:

htmlElement.appendChild(view.target);

You can update scalingMode by invoking the updateScalingMode method:

view.updateScalingMode('Crop')

Device management

In deviceManager, you can enumerate local devices that can transmit your audio and video streams in a call. You can also use it to request permission to access the local device's microphones and cameras.

You can access deviceManager by calling the callClient.getDeviceManager() method:

const deviceManager = await callClient.getDeviceManager();

Get local devices

To access local devices, you can use enumeration methods on deviceManager. Enumeration is an asynchronous action

//  Get a list of available video devices for use.
const localCameras = await deviceManager.getCameras(); // [VideoDeviceInfo, VideoDeviceInfo...]

// Get a list of available microphone devices for use.
const localMicrophones = await deviceManager.getMicrophones(); // [AudioDeviceInfo, AudioDeviceInfo...]

// Get a list of available speaker devices for use.
const localSpeakers = await deviceManager.getSpeakers(); // [AudioDeviceInfo, AudioDeviceInfo...]

Set the default microphone and speaker

In deviceManager, you can set a default device that you'll use to start a call. If client defaults aren't set, Communication Services uses operating system defaults.

// Get the microphone device that is being used.
const defaultMicrophone = deviceManager.selectedMicrophone;

// Set the microphone device to use.
await deviceManager.selectMicrophone(localMicrophones[0]);

// Get the speaker device that is being used.
const defaultSpeaker = deviceManager.selectedSpeaker;

// Set the speaker device to use.
await deviceManager.selectSpeaker(localSpeakers[0]);

Local camera preview

You can use deviceManager and VideoStreamRenderer to begin rendering streams from your local camera. This stream won't be sent to other participants; it's a local preview feed.

const cameras = await deviceManager.getCameras();
const camera = cameras[0];
const localCameraStream = new LocalVideoStream(camera);
const videoStreamRenderer = new VideoStreamRenderer(localCameraStream);
const view = await videoStreamRenderer.createView();
htmlElement.appendChild(view.target);

Request permission to camera and microphone

Prompt a user to grant camera and microphone permissions:

const result = await deviceManager.askDevicePermission({audio: true, video: true});

This resolves with an object that indicates whether audio and video permissions were granted:

console.log(result.audio);
console.log(result.video);

Record calls

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 ACS Calling Web SDK

Call recording is an extended feature of the core Call API. You first need to obtain the recording feature API object:

const callRecordingApi = call.api(Features.Recording);

Then, to check if the call is being recorded, inspect the isRecordingActive property of callRecordingApi. It returns Boolean.

const isResordingActive = callRecordingApi.isRecordingActive;

You can also subscribe to recording changes:

const isRecordingActiveChangedHandler = () => {
  console.log(callRecordingApi.isRecordingActive);
};

callRecordingApi.on('isRecordingActiveChanged', isRecordingActiveChangedHandler);

Transfer calls

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 ACS Calling Web SDK

Call transfer is an extended feature of the core Call API. You first need to get the transfer feature API object:

const callTransferApi = call.api(Features.Transfer);

Call transfers involve three parties:

  • Transferor: The person who initiates the transfer request.
  • Transferee: The person who is being transferred.
  • Transfer target: The person who is being transferred to.

Transfers follow these steps:

  1. There's already a connected call between the transferor and the transferee. The transferor decides to transfer the call from the transferee to the transfer target.
  2. The transferor calls the transfer API.
  3. The transferee decides whether to accept or reject the transfer request to the transfer target by using a transferRequested event.
  4. The transfer target receives an incoming call only if the transferee accepts the transfer request.

To transfer a current call, you can use the transfer API. transfer takes the optional transferCallOptions, which allows you to set a disableForwardingAndUnanswered flag:

  • disableForwardingAndUnanswered = false: If the transfer target doesn't answer the transfer call, the transfer follows the transfer target forwarding and unanswered settings.
  • disableForwardingAndUnanswered = true: If the transfer target doesn't answer the transfer call, the transfer attempt ends.
// transfer target can be an ACS user
const id = { communicationUserId: <ACS_USER_ID> };
// call transfer API
const transfer = callTransferApi.transfer({targetParticipant: id});

The transfer API allows you to subscribe to transferStateChanged and transferRequested events. A transferRequested event comes from a call instance; a transferStateChanged event and transfer state and error come from a transfer instance.

// transfer state
const transferState = transfer.state; // None | Transferring | Transferred | Failed

// to check the transfer failure reason
const transferError = transfer.error; // transfer error code that describes the failure if a transfer request failed

The transferee can accept or reject the transfer request initiated by the transferor in the transferRequested event by using accept() or reject() in transferRequestedEventArgs. You can access targetParticipant information and accept or reject methods in transferRequestedEventArgs.

// Transferee to accept the transfer request
callTransferApi.on('transferRequested', args => {
  args.accept();
});

// Transferee to reject the transfer request
callTransferApi.on('transferRequested', args => {
  args.reject();
});

Learn about eventing models

Inspect current values and subscribe to update events for future values.

Properties

// Inspect the current value
console.log(object.property);

// Subscribe to value updates
object.on('propertyChanged', () => {
    // Inspect new value
    console.log(object.property)
});

// Unsubscribe from updates:
object.off('propertyChanged', () => {});



// Example for inspecting a call state
console.log(call.state);
call.on('stateChanged', () => {
    console.log(call.state);
});
call.off('stateChanged', () => {});

Collections

// Inspect the current collection
object.collection.forEach(v => {
    console.log(v);
});

// Subscribe to collection updates
object.on('collectionUpdated', e => {
    // Inspect new values added to the collection
    e.added.forEach(v => {
        console.log(v);
    });
    // Inspect values removed from the collection
    e.removed.forEach(v => {
        console.log(v);
    });
});

// Unsubscribe from updates:
object.off('collectionUpdated', () => {});

// Example for subscribing to remote participants and their video streams
call.remoteParticipants.forEach(p => {
    subscribeToRemoteParticipant(p);
})

call.on('remoteParticipantsUpdated', e => {
    e.added.forEach(p => { subscribeToRemoteParticipant(p) })
    e.removed.forEach(p => { unsubscribeFromRemoteParticipant(p) })
});

function subscribeToRemoteParticipant(p) {
    console.log(p.state);
    p.on('stateChanged', () => { console.log(p.state); });
    p.videoStreams.forEach(v => { subscribeToRemoteVideoStream(v) });
    p.on('videoStreamsUpdated', e => { e.added.forEach(v => { subscribeToRemoteVideoStream(v) }) })
}

Prerequisites

Setting up

Install the package

Note

This document uses version 1.0.0 of the Calling 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'
    ...
}

Object model

The following classes and interfaces handle some of the major features of the Azure Communication Services Calling SDK:

Name Description
CallClient The CallClient is the main entry point to the Calling SDK.
CallAgent The CallAgent is used to start and manage calls.
CommunicationTokenCredential The CommunicationTokenCredential is used as the token credential to instantiate the CallAgent.
CommunicationIdentifier The CommunicationIdentifier is used as different type of participant that would could be part of a call.

Initialize the CallClient, create a CallAgent, and access the DeviceManager

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 an outgoing call and join a group 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

Warning

Currently PSTN calling is not available

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);

Place a 1:1 call with video camera

Warning

Currently only one outgoing local video stream is supported To place a call with video you have to enumerate local cameras using the deviceManager getCameras API. Once you select a desired camera, use it to construct a LocalVideoStream instance and pass it into videoOptions as an item in the localVideoStream array to a call method. Once the call connects it'll automatically start sending a video stream from the selected camera to other participant(s).

Note

Due to privacy concerns, video will not be shared to the call if it is not being previewed locally. See Local camera preview for more details.

Context appContext = this.getApplicationContext();
VideoDeviceInfo desiredCamera = callClient.getDeviceManager(appContext).get().getCameras().get(0);
LocalVideoStream currentVideoStream = new LocalVideoStream(desiredCamera, appContext);
LocalVideoStream[] localVideoStreams = new LocalVideoStream[1];
localVideoStreams[0] = currentVideoStream;
VideoOptions videoOptions = new VideoOptions(localVideoStreams);

// Render a local preview of video so the user knows that their video is being shared
Renderer previewRenderer = new VideoStreamRenderer(currentVideoStream, appContext);
View uiView = previewRenderer.createView(new CreateViewOptions(ScalingMode.FIT));
// Attach the uiView to a viewable location on the app at this point
layout.addView(uiView);

CommunicationUserIdentifier[] participants = new CommunicationUserIdentifier[]{ new CommunicationUserIdentifier("<acs user id>") };
StartCallOptions startCallOptions = new StartCallOptions();
startCallOptions.setVideoOptions(videoOptions);
Call call = callAgent.startCall(context, participants, startCallOptions);

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);

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;
}

Push notifications

Overview

Mobile push notifications are the pop-up notifications you see on mobile devices. For calling, we'll be focusing on VoIP (Voice over Internet Protocol) push notifications. We'll register for push notifications, handle push notifications, and then un-register push notifications.

Prerequisites

A Firebase account set up with Cloud Messaging (FCM) enabled and with your Firebase Cloud Messaging service connected to an Azure Notification Hub instance. See Communication Services notifications for more information. Additionally, the tutorial assumes you're using Android Studio version 3.6 or higher to build your application.

A set of permissions is required for the Android application in order to be able to receive notifications messages from Firebase Cloud Messaging. In your AndroidManifest.xml file, add the following set of permissions right after the <manifest ...> or below the tag

    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.GET_ACCOUNTS"/>
    <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />

Register for push notifications

To register for push notifications, the application needs to call registerPushNotification() on a CallAgent instance with a device registration token.

To obtain the device registration token, add the Firebase SDK to your application module's build.gradle file by adding the following lines in the dependencies section if it's not already there:

    // Add the SDK for Firebase Cloud Messaging
    implementation 'com.google.firebase:firebase-core:16.0.8'
    implementation 'com.google.firebase:firebase-messaging:20.2.4'

In your project level's build.gradle file, add the following in the dependencies section if it's not already there:

    classpath 'com.google.gms:google-services:4.3.3'

Add the following plugin to the beginning of the file if it's not already there:

apply plugin: 'com.google.gms.google-services'

Select Sync Now in the toolbar. Add the following code snippet to get the device registration token generated by the Firebase Cloud Messaging SDK for the client application instance Be sure to add the below imports to the header of the main Activity for the instance. They're required for the snippet to retrieve the token:

import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import com.google.firebase.iid.FirebaseInstanceId;
import com.google.firebase.iid.InstanceIdResult;

Add this snippet to retrieve the token:

        FirebaseInstanceId.getInstance().getInstanceId()
                .addOnCompleteListener(new OnCompleteListener<InstanceIdResult>() {
                    @Override
                    public void onComplete(@NonNull Task<InstanceIdResult> task) {
                        if (!task.isSuccessful()) {
                            Log.w("PushNotification", "getInstanceId failed", task.getException());
                            return;
                        }

                        // Get new Instance ID token
                        String deviceToken = task.getResult().getToken();
                        // Log
                        Log.d("PushNotification", "Device Registration token retrieved successfully");
                    }
                });

Register the device registration token with the Calling Services SDK for incoming call push notifications:

String deviceRegistrationToken = "<Device Token from previous section>";
try {
    callAgent.registerPushNotification(deviceRegistrationToken).get();
}
catch(Exception e) {
    System.out.println("Something went wrong while registering for Incoming Calls Push Notifications.")
}

Push notification handling

To receive incoming call push notifications, call handlePushNotification() on a CallAgent instance with a payload.

To obtain the payload from Firebase Cloud Messaging, begin by creating a new Service (File > New > Service > Service) that extends the FirebaseMessagingService Firebase SDK class and override the onMessageReceived method. This method is the event handler called when Firebase Cloud Messaging delivers the push notification to the application.

public class MyFirebaseMessagingService extends FirebaseMessagingService {
    private java.util.Map<String, String> pushNotificationMessageDataFromFCM;

    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        // Check if message contains a notification payload.
        if (remoteMessage.getNotification() != null) {
            Log.d("PushNotification", "Message Notification Body: " + remoteMessage.getNotification().getBody());
        }
        else {
            pushNotificationMessageDataFromFCM = remoteMessage.getData();
        }
    }
}

Add the following service definition to the AndroidManifest.xml file, inside the tag:

        <service
            android:name=".MyFirebaseMessagingService"
            android:exported="false">
            <intent-filter>
                <action android:name="com.google.firebase.MESSAGING_EVENT" />
            </intent-filter>
        </service>
  • Once the payload is retrieved, it can be passed to the Communication Services SDK to be parsed out into an internal IncomingCallInformation object that will be handled by calling the handlePushNotification method on a CallAgent instance. A CallAgent instance is created by calling the createCallAgent(...) method on the CallClient class.
try {
    IncomingCallInformation notification = IncomingCallInformation.fromMap(pushNotificationMessageDataFromFCM);
    Future handlePushNotificationFuture = callAgent.handlePushNotification(notification).get();
}
catch(Exception e) {
    System.out.println("Something went wrong while handling the Incoming Calls Push Notifications.");
}

When the handling of the Push notification message is successful, and the all events handlers are registered properly, the application will ring.

Unregister push notifications

Applications can unregister push notification at any time. Call the unregisterPushNotification() method on callAgent to unregister.

try {
    callAgent.unregisterPushNotification().get();
}
catch(Exception e) {
    System.out.println("Something went wrong while un-registering for all Incoming Calls Push Notifications.")
}

Call Management

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

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 see if the current call is being recorded, inspect the isRecordingActive property:

boolean recordingActive = call.isRecordingActive();

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();

Start and stop sending local video

To start a video, you have to enumerate cameras using the getCameraList API on deviceManager object. Then create a new instance of LocalVideoStream passing the desired camera, and pass it in the startVideo API as an argument:

VideoDeviceInfo desiredCamera = <get-video-device>;
Context appContext = this.getApplicationContext();
LocalVideoStream currentLocalVideoStream = new LocalVideoStream(desiredCamera, appContext);
VideoOptions videoOptions = new VideoOptions(currentLocalVideoStream);
Future startVideoFuture = call.startVideo(appContext, currentLocalVideoStream);
startVideoFuture.get();

Once you successfully start sending video, a LocalVideoStream instance will be added to the localVideoStreams collection on the call instance.

currentLocalVideoStream == call.getLocalVideoStreams().get(0);

To stop local video, pass the LocalVideoStream instance available in localVideoStreams collection:

call.stopVideo(appContext, currentLocalVideoStream).get();

You can switch to a different camera device while video is being sent by invoking switchSource on a LocalVideoStream instance:

currentLocalVideoStream.switchSource(source).get();

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....]

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, ...]

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();

Render remote participant video streams

To list the video streams and screen sharing streams of remote participants, inspect the videoStreams collections:

RemoteParticipant remoteParticipant = call.getRemoteParticipants().get(0);
RemoteVideoStream remoteParticipantStream = remoteParticipant.getVideoStreams().get(0);
MediaStreamType streamType = remoteParticipantStream.getType(); // of type MediaStreamType.Video or MediaStreamType.ScreenSharing

To render a RemoteVideoStream from a remote participant, you have to subscribe to a OnVideoStreamsUpdated event.

Within the event, the change of isAvailable property to true indicates that remote participant is currently sending a stream. Once that happens, create new instance of a Renderer, then create a new RendererView using asynchronous createView API and attach view.target anywhere in the UI of your application.

Whenever availability of a remote stream changes you can choose to destroy the whole Renderer, a specific RendererView or keep them, but this will result in displaying blank video frame.

VideoStreamRenderer remoteVideoRenderer = new VideoStreamRenderer(remoteParticipantStream, appContext);
VideoStreamRendererView uiView = remoteVideoRenderer.createView(new RenderingOptions(ScalingMode.FIT));
layout.addView(uiView);

remoteParticipant.addOnVideoStreamsUpdatedListener(e -> onRemoteParticipantVideoStreamsUpdated(p, e));

void onRemoteParticipantVideoStreamsUpdated(RemoteParticipant participant, RemoteVideoStreamsEvent args) {
    for(RemoteVideoStream stream : args.getAddedRemoteVideoStreams()) {
        if(stream.getIsAvailable()) {
            startRenderingVideo();
        } else {
            renderer.dispose();
        }
    }
}

Remote video stream properties

Remote video stream has couple of properties

  • Id - ID of a remote video stream
int id = remoteVideoStream.getId();
  • MediaStreamType - Can be 'Video' or 'ScreenSharing'
MediaStreamType type = remoteVideoStream.getMediaStreamType();
  • isAvailable - Indicates if remote participant endpoint is actively sending stream
boolean availability = remoteVideoStream.isAvailable();

Renderer methods and properties

Renderer object following APIs

  • Create a VideoStreamRendererView instance that can be later attached in the application UI to render remote video stream.
// Create a view for a video stream
VideoStreamRendererView.createView()
  • Dispose renderer and all VideoStreamRendererView associated with this renderer. To be called when you have removed all associated views from the UI.
VideoStreamRenderer.dispose()
  • StreamSize - size (width/height) of a remote video stream
StreamSize renderStreamSize = VideoStreamRenderer.getSize();
int width = renderStreamSize.getWidth();
int height = renderStreamSize.getHeight();

RendererView methods and properties

When creating a VideoStreamRendererView you can specify the ScalingMode and mirrored properties that will apply to this view: Scaling mode can be either of 'CROP' | 'FIT'

VideoStreamRenderer remoteVideoRenderer = new VideoStreamRenderer(remoteVideoStream, appContext);
VideoStreamRendererView rendererView = remoteVideoRenderer.createView(new CreateViewOptions(ScalingMode.Fit));

The created RendererView can then be attached to the application UI using the following snippet:

layout.addView(rendererView);

You can later update the scaling mode by invoking updateScalingMode API on the RendererView object with one of ScalingMode.CROP | ScalingMode.FIT as an argument.

// Update the scale mode for this view.
rendererView.updateScalingMode(ScalingMode.CROP)

Device management

DeviceManager lets you enumerate local devices that can be used in a call to transmit your audio/video streams. It also allows you to request permission from a user to access their microphone and camera using the native browser API.

You can access deviceManager by calling callClient.getDeviceManager() method.

Warning

Currently a callAgent object must be instantiated first in order to gain access to DeviceManager

Context appContext = this.getApplicationContext();
DeviceManager deviceManager = callClient.getDeviceManager(appContext).get();

Enumerate local devices

To access local devices, you can use enumeration methods on the Device Manager. Enumeration is a synchronous action.

//  Get a list of available video devices for use.
List<VideoDeviceInfo> localCameras = deviceManager.getCameras(); // [VideoDeviceInfo, VideoDeviceInfo...]

Local camera preview

You can use DeviceManager and Renderer to begin rendering streams from your local camera. This stream won't be sent to other participants; it's a local preview feed. This is an asynchronous action.

VideoDeviceInfo videoDevice = <get-video-device>;
Context appContext = this.getApplicationContext();
currentVideoStream = new LocalVideoStream(videoDevice, appContext);
LocalVideoStream[] localVideoStreams = new LocalVideoStream[1];
localVideoStreams[0] = currentVideoStream;
videoOptions = new VideoOptions(localVideoStreams);

VideoStreamRenderer previewRenderer = new VideoStreamRenderer(currentVideoStream, appContext);
VideoStreamRendererView uiView = previewRenderer.createView(new RenderingOptions(ScalingMode.Fit));

// Attach the uiView to a viewable location on the app at this point
layout.addView(uiView);

Eventing model

You can subscribe to most of the properties and collections to be notified when values change.

Properties

To subscribe to property changed events:

// subscribe
PropertyChangedListener callStateChangeListener = new PropertyChangedListener()
{
    @Override
    public void onPropertyChanged(PropertyChangedEvent args)
    {
        Log.d("The call state has changed.");
    }
}
call.addOnStateChangedListener(callStateChangeListener);

//unsubscribe
call.removeOnStateChangedListener(callStateChangeListener);

Collections

To subscribe to collection updated events:

LocalVideoStreamsChangedListener localVideoStreamsChangedListener = new LocalVideoStreamsChangedListener()
{
    @Override
    public void onLocalVideoStreamsUpdated(LocalVideoStreamsEvent localVideoStreamsEventArgs) {
        Log.d(localVideoStreamsEventArgs.getAddedStreams().size());
        Log.d(localVideoStreamsEventArgs.getRemovedStreams().size());
    }
}
call.addOnLocalVideoStreamsChangedListener(localVideoStreamsChangedListener);
// To unsubscribe
call.removeOnLocalVideoStreamsChangedListener(localVideoStreamsChangedListener);

Prerequisites

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

Learn the object model

The following classes and interfaces handle some of the major features of the Azure Communication Services Calling SDK for iOS.

Note

This quickstart uses version 1.0.0-beta.8 of the Calling SDK.

Name Description
CallClient CallClient is the main entry point to the Calling SDK.
CallAgent CallAgent is used to start and manage calls.
CommunicationTokenCredential CommunicationTokenCredential is used as the token credential to instantiate CallAgent.
CommunicationIdentifier CommunicationIdentifier is used to represent the identity of the user. The identity can be CommunicationUserIdentifier, PhoneNumberIdentifier, or CallingApplication.

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.

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 ACS 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")
        }
})

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

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")
     }
}

Place a 1:1 call with video

To get a device manager instance, see the section about managing devices.


let firstCamera = self.deviceManager!.cameras.first
self.localVideoStreams = [LocalVideoStream]()
self.localVideoStreams!.append(LocalVideoStream(camera: firstCamera!))
let videoOptions = VideoOptions(localVideoStreams: self.localVideoStreams!)

let startCallOptions = StartCallOptions()
startCallOptions.videoOptions = videoOptions

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

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, didRecieveIncomingCall 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")
}

Set up push notifications

A mobile push notification is the pop-up notification that you get in the mobile device. For calling, we'll focus on VoIP (voice over Internet Protocol) push notifications.

The following sections describe how to register for, handle, and unregister push notifications. Before you start those tasks, complete these prerequisites:

  1. In Xcode, go to Signing & Capabilities. Add a capability by selecting + Capability, and then select Push Notifications.
  2. Add another capability by selecting + Capability, and then select Background Modes.
  3. Under Background Modes, select the Voice over IP and Remote notifications checkboxes.

Screenshot that shows how to add capabilities in Xcode.

Register for push notifications

To register for push notifications, call registerPushNotification() on a CallAgent instance with a device registration token.

Registration for push notifications needs to happen after successful initialization. When the callAgent object is destroyed, logout will be called, which will automatically unregister push notifications.


let deviceToken: Data = pushRegistry?.pushToken(for: PKPushType.voIP)
callAgent.registerPushNotifications(deviceToken: deviceToken!) { (error) in
    if(error == nil) {
        print("Successfully registered to push notification.")
    } else {
        print("Failed to register push notification.")
    }
}

Handle push notifications

To receive push notifications for incoming calls, call handlePushNotification() on a CallAgent instance with a dictionary payload.


let callNotification = PushNotificationInfo.fromDictionary(pushPayload.dictionaryPayload)

callAgent.handlePush(notification: callNotification) { (error) in
    if (error == nil) {
        print("Handling of push notification was successful")
    } else {
        print("Handling of push notification failed")
    }
}

Unregister push notifications

Applications can unregister push notification at any time. Simply call the unregisterPushNotification method on CallAgent.

Note

Applications are not automatically unregistered from push notifications on logout.


callAgent.unregisterPushNotification { (error) in
    if (error == nil) {
        print("Unregister of push notification was successful")
    } else {
       print("Unregister of push notification failed, please try again")
    }
}

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")
    }
}

Start and stop sending local video

To start sending local video to other participants in a call, use the startVideo API and pass localVideoStream with camera.


let firstCamera: VideoDeviceInfo? = self.deviceManager!.cameras.first
let localVideoStream = LocalVideoStream(camera: firstCamera!)

call!.startVideo(stream: localVideoStream) { (error) in
    if (error == nil) {
        print("Local video started successfully")
    } else {
        print("Local video failed to start")
    }
}

After you start sending video, the LocalVideoStream instance is added the localVideoStreams collection on a call instance.


call.localVideoStreams

To stop local video, pass the localVideoStream instance returned from the invocation of call.startVideo. This is an asynchronous action.


call!.stopVideo(stream: localVideoStream) { (error) in
    if (error == nil) {
        print("Local video stopped successfully")
    } else {
        print("Local video failed to stop")
    }
}

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

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, ...]

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")
    }
}

Render remote participant video streams

Remote participants can initiate video or screen sharing during a call.

Handle video-sharing or screen-sharing streams of remote participants

To list the streams of remote participants, inspect the videoStreams collections.


var remoteParticipantVideoStream = call.remoteParticipants[0].videoStreams[0]

Get remote video stream properties


var type: MediaStreamType = remoteParticipantVideoStream.type // 'MediaStreamTypeVideo'

var isAvailable: Bool = remoteParticipantVideoStream.isAvailable // indicates if remote stream is available

var id: Int = remoteParticipantVideoStream.id // id of remoteParticipantStream

Render remote participant streams

To start rendering remote participant streams, use the following code.


let renderer = VideoStreamRenderer(remoteVideoStream: remoteParticipantVideoStream)
let targetRemoteParticipantView = renderer?.createView(withOptions: CreateViewOptions(scalingMode: ScalingMode.crop))
// To update the scaling mode later
targetRemoteParticipantView.update(scalingMode: ScalingMode.fit)

Get remote video renderer methods and properties

// [Synchronous] dispose() - dispose renderer and all `RendererView` associated with this renderer. To be called when you have removed all associated views from the UI.
remoteVideoRenderer.dispose()

Manage devices

DeviceManager lets you enumerate local devices that can be used in a call to transmit audio or video streams. It also allows you to request permission from a user to access a microphone or camera. You can access deviceManager on the callClient object.


self.callClient!.getDeviceManager { (deviceManager, error) in
        if (error == nil) {
            print("Got device manager instance")
            self.deviceManager = deviceManager
        } else {
            print("Failed to get device manager instance")
        }
    }

Enumerate local devices

To access local devices, you can use enumeration methods on the device manager. Enumeration is a synchronous action.

// enumerate local cameras
var localCameras = deviceManager.cameras // [VideoDeviceInfo, VideoDeviceInfo...]

Get a local camera preview

You can use Renderer to begin rendering a stream from your local camera. This stream won't be sent to other participants; it's a local preview feed. This is an asynchronous action.


let camera: VideoDeviceInfo = self.deviceManager!.cameras.first!
let localVideoStream = LocalVideoStream(camera: camera)
let localRenderer = try! VideoStreamRenderer(localVideoStream: localVideoStream)
self.view = try! localRenderer.createView()

Get local camera preview properties

The renderer has set of properties and methods that allow you to control the rendering.


// Constructor can take in LocalVideoStream or RemoteVideoStream
let localRenderer = VideoStreamRenderer(localVideoStream:localVideoStream)
let remoteRenderer = VideoStreamRenderer(remoteVideoStream:remoteVideoStream)

// [StreamSize] size of the rendering view
localRenderer.size

// [VideoStreamRendererDelegate] an object you provide to receive events from this Renderer instance
localRenderer.delegate

// [Synchronous] create view
try! localRenderer.createView()

// [Synchronous] create view with rendering options
try! localRenderer!.createView(withOptions: CreateViewOptions(scalingMode: ScalingMode.fit))

// [Synchronous] dispose rendering view
localRenderer.dispose()

Subscribe to notifications

You can subscribe to most of the properties and collections to be notified when values change.

Properties

To subscribe to property changed events, use the following code.

call.delegate = self
// Get the property of the call state by getting on the call's state member
public func call(_ call: Call, didChangeState args: PropertyChangedEventArgs) {
{
    print("Callback from SDK when the call state changes, current state: " + call.state.rawValue)
}

 // to unsubscribe
 self.call.delegate = nil

Collections

To subscribe to collection updated events, use the following code.

call.delegate = self
// Collection contains the streams that were added or removed only
public func call(_ call: Call, didUpdateLocalVideoStreams args: LocalVideoStreamsUpdatedEventArgs) {
{
    print(args.addedStreams.count)
    print(args.removedStreams.count)
}
// to unsubscribe
self.call.delegate = nil

Clean up resources

If you want to clean up and remove a Communication Services subscription, you can delete the resource or resource group. Deleting the resource group also deletes any other resources associated with it. Learn more about cleaning up resources.

Next steps

For more information, see the following articles: