@azure/communication-calling

Azure Communication Services Calling Web SDK

Usage no npm install needed!

<script type="module">
  import azureCommunicationCalling from 'https://cdn.skypack.dev/@azure/communication-calling';
</script>

README

Azure Communication Calling client library for JavaScript

Get started with Azure Communication Services by using the Communication Services calling client library to add voice and video calling to your app. Read more about Azure Communication Services here

Getting started

Prerequisites

Setting up

Install the client library

Use the npm install command to install the Azure Communication Services Calling and Common client libraries 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 client library:

Name Description
CallClient The CallClient is the main entry point to the Calling client library.
CallAgent The CallAgent is used to start and manage calls.
DeviceManager The DeviceManager is used to manage media devices
AzureCommunicationTokenCredential The AzureCommunicationTokenCredential class implements the CommunicationTokenCredential interface which is used to instantiate the CallAgent.

Initialize the CallClient, create CallAgent, and access DeviceManager

Instantiate a new CallClient instance. You can configure it with custom options like a Logger instance. Once a CallClient is instantiated, 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 takes a CommunicationTokenCredential as an argument, which accepts a user access token.

// 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 an outgoing call

To create and start a call you need to use one of the APIs on CallAgent and provide a user that you've created through the Communication Services administration client library.

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

Place a call

Place a 1:1 call to a user or PSTN

To place a call to another Communication Services user, invoke the startCall method on callAgent and pass the callee's CommunicationUserIdentifier that you've created with the Communication Services Administration library.

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

To place a call to a PSTN, invoke the startCall method on callAgent and pass the callee's PhoneNumberIdentifier. Your Communication Services resource must be configured to allow PSTN calling. When calling a PSTN number, you must specify your alternate caller ID. An alternate caller Id refers to a phone number (based on the E.164 standard) identifiying the caller in a PSTN Call. For example, when you supply an alternate caller Id to the PSTN call, that phone number will be the one shown to the callee when the call is incoming.

[!WARNING] PSTN calling is currently in private preview. For access, apply to early adopter program.

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

Place a 1:n call with users and PSTN

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

[!WARNING] There can currently be no more than one outgoing local video stream. To place a video call, you have to enumerate local cameras using the deviceManager getCameras() API. Once you select the desired camera, use it to construct a LocalVideoStream instance and pass it within videoOptions as an item within the localVideoStream array to the startCall method. Once your call connects it'll automatically start sending a video stream from the selected camera to the other participant(s). This also applies to the Call.Accept() video options and CallAgent.join() video options.

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

Join a group call

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 value has to be a GUID.


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

Receiving an incoming call

The CallAgent instance emits an incomingCall event when the logged in identity is receiving an incoming call. To listen to this event, subscribe in the following way:

const incomingCallHander = async (args: { incomingCall: IncomingCall }) => {

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

    // 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 will provide with an instance of IncomingCall on which you can accept or reject a call.

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 (string) for this Call.

const callId: string = call.id;

  • To learn about other participants in the call, inspect the remoteParticipants collection on the call instance. Array contains list RemoteParticipant objects
const remoteParticipants = call.remoteParticipants;
  • The identifier of caller if the call is incoming. Identifier is one of the CommunicationIdentifier types

const callerIdentity = call.callerInfo.identifier;

  • Get the state of the Call.

const callState = call.state;

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

  • 'None' - initial call state

  • 'Connecting' - initial transition state once call is placed or accepted

  • 'Ringing' - for an outgoing call - indicates call is ringing for remote participants

  • 'EarlyMedia' - indicates a state in which an announcement is played before the call is connected

  • 'Connected' - call is connected

  • 'LocalHold' - call is put on hold by local participant, no media is flowing between local endpoint and remote participant(s)

  • 'RemoteHold' - call is put on hold by remote participant, no media is flowing between local endpoint and remote participant(s)

  • 'Disconnecting' - transition state before the call goes to 'Disconnected' state

  • 'Disconnected' - final call state

    • If network connection is lost, state goes to 'Disconnected' after about 2 minutes.
  • To see why a given call ended, inspect 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
  • To learn if the current call is an incoming or outgoing call, inspect the direction property, it returns CallDirection.
const isIncoming = call.direction == 'Incoming';
const isOutgoing = call.direction == 'Outgoing';
  • To check if the current microphone is muted, inspect the isMuted property, it returns Boolean.

const muted = call.isMuted;

  • To see if the screen sharing stream is being sent from a given endpoint, check the isScreenSharingOn property, it returns Boolean.

const isScreenSharingOn = call.isScreenSharingOn;

  • To inspect active video streams, check the localVideoStreams collection, it contains 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 passing the desired camera into the startVideo method as an argument:

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

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


call.localVideoStreams[0] === localVideoStream;

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


await call.stopVideo(localVideoStream);

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

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

Remote participants management

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

List participants in a call

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


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

Remote participant properties

Remote participant has a set of properties and collections associated with it

CommunicationIdentifier

Get the identifier for this remote participant. Identity is one of the 'CommunicationIdentifier' types:

const identifier = remoteParticipant.identifier;

It can be one of 'CommunicationIdentifier' types:

  • { communicationUserId: '<ACS_USER_ID'> } - object representing ACS User
  • { phoneNumber: '<E.164>' } - object representing phone number in E.164 format
  • { microsoftTeamsUserId: '<TEAMS_USER_ID>', isAnonymous?: boolean; cloud?: "public" | "dod" | "gcch" } - object representing Teams user
  • { id: string } - object repredenting identifier that doesn't fit any of the other identifier types

State

Get state of this remote participant.


const state = remoteParticipant.state;

State can be one of

  • 'Idle' - initial state
  • 'Connecting' - transition state while participant is connecting to the call
  • 'Ringing' - participant is ringing
  • 'Connected' - participant is connected to the call
  • 'Hold' - participant is on hold
  • 'EarlyMedia' - announcement is played before participant is connected to the call
  • 'Disconnected' - final state - participant is disconnected from the call
    • If remote participant loses their network connectivity, then remote participant state goes to 'Disconnected' after about 2 minutes.

Call End reason

To learn why participant left the call, inspect 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

Is Muted

To check whether this remote participant is muted or not, inspect isMuted property, it returns Boolean

const isMuted = remoteParticipant.isMuted;

Is Speaking

To check whether this remote participant is speaking or not, inspect isSpeaking property it returns Boolean

const isSpeaking = remoteParticipant.isSpeaking;

Video Streams

To inspect all video streams that a given participant is sending in this call, check videoStreams collection, it contains RemoteVideoStream objects


const videoStreams = remoteParticipant.videoStreams; // [RemoteVideoStream, ...]

Display Name

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 to a call (either a user or a phone number) you can invoke addParticipant. Provide one of the 'Identifier' types. This will synchronously return the remote participant instance.

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 participant from a call

To remove a participant from a call (either a user or a phone number) you can invoke removeParticipant. You have to pass one of the 'Identifier' types This will resolve asynchronously once the participant is removed from the call. The participant will also be 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 a RemoteVideoStream, you have to subscribe to a isAvailableChanged event. If the isAvailable property changes to true, a remote participant is sending a stream. Once that happens, create a new instance of VideoStreamRenderer, and then create a new VideoStreamRendererView instance using the asynchronous createView method. You may 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('availabilityChanged', async () => {
        if (remoteVideoStream.isAvailable) {
            displayVideo();
        } else {
            videoStreamRenderer.dispose();
        }
    });
    if (remoteVideoStream.isAvailable) {
        displayVideo();
    }
}

Remote video stream properties

Remote video streams have the following properties:

  • Id - ID of a remote video stream
const id: number = remoteVideoStream.id;
  • MediaStreamType - can be 'Video' or 'ScreenSharing'
const type: MediaStreamType = remoteVideoStream.mediaStreamType;
  • isAvailable - Indicates if remote participant endpoint is actively sending stream
const type: boolean = remoteVideoStream.isAvailable;

VideoStreamRenderer methods and properties

  • StreamSize - dimensions ( width/height ) of the renderer
const size: {width: number; height: number} = videoStreamRenderer.size;
  • Create a VideoStreamRendererView instance that can be later attached in the application UI to render the remote video stream.
videoStreamRenderer.createView()
  • Dispose of the videoStreamRenderer and all associated VideoStreamRendererView instances.
videoStreamRenderer.dispose()

VideoStreamRendererView methods and properties

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

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

Any given VideoStreamRendererView instance has a target property that represents the rendering surface. This has to be attached in the application UI:

htmlElement.appendChild(view.target);

You can later update the scaling mode by invoking the updateScalingMode method.

view.updateScalingMode('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 the deviceManager by calling callClient.getDeviceManager() method.


const deviceManager = await callClient.getDeviceManager();

Enumerate local devices

To access local devices, you can use enumeration methods on the Device Manager. 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 default microphone/speaker

Device manager allows you to set a default device that will be used when starting a call. If client defaults are not set, Communication Services will fall back to OS 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. This is an asynchronous action.

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/microphone

Prompt a user to grant camera/microphone permissions with the following:

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

This will resolve asynchronously with an object indicating if audio and video permissions were granted:

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

Eventing model

You have to inspect current values and subscribe to update events for future values.

Properties

// Inspect 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 call state
console.log(call.state);
call.on('stateChanged', () => {
    console.log(call.state);
});
call.off('stateChanged', () => {});

Collections

// Inspect 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) }) })
}