Proposed Pull Request Change

author ms.service ms.topic ms.date ms.author ms.custom
probableprime azure-communication-services include 09/13/2023 rifox sfi-ropc-nochange
📄 Document Links
GitHub View on GitHub Microsoft Learn View on Microsoft Learn
Raw New Markdown
Generating updated version of doc...
Rendered New Markdown
Generating updated version of doc...
+0 -0
+0 -0
--- author: probableprime ms.service: azure-communication-services ms.topic: include ms.date: 09/13/2023 ms.author: rifox ms.custom: sfi-ropc-nochange --- Get started with Azure Communication Services by using the Communication Services calling SDK to add 1 on 1 video calling to your app. You learn how to start and answer a video call using the Azure Communication Services Calling SDK for JavaScript. ## Sample Code If you'd like to skip ahead to the end, you can download this quickstart as a sample on [GitHub](https://github.com/Azure-Samples/communication-services-javascript-quickstarts/tree/main/add-1-on-1-video-calling). > [!NOTE] > Outbound calling to an Azure Communication Services user can be accessed using the [Azure Communication Services UI Library](https://azure.github.io/communication-ui-library/?path=/docs/composites-callcomposite-1-n-docs--docs). The UI Library enables developers to add a call client that is VoIP enabled into their application with only a couple lines of code. ## Prerequisites - Obtain an Azure account with an active subscription. [Create an account for free](https://azure.microsoft.com/pricing/purchase-options/azure-account?cid=msft_learn). - You need to have [Node.js 18](https://nodejs.org/dist/v18.18.0/). You can use the msi installer to install it. - Create an active Communication Services resource. [Create a Communication Services resource](../../../create-communication-resource.md?pivots=platform-azp&tabs=windows). You need to **record your connection string** for this quickstart. - Create a User Access Token to instantiate the call client. [Learn how to create and manage user access tokens](../../../identity/access-tokens.md). You can also use the Azure CLI and run the command with your connection string to create a user and an access token. ```azurecli-interactive az communication identity token issue --scope voip --connection-string "yourConnectionString" ``` For details, see [Use Azure CLI to Create and Manage Access Tokens](../../../identity/access-tokens.md?pivots=platform-azcli). ## Setting up ### Create a new Node.js application Open your terminal or command window create a new directory for your app, and navigate to it. ```console mkdir calling-quickstart && cd calling-quickstart ``` Run `npm init -y` to create a **package.json** file with default settings. ```console npm init -y ``` ### Install the package Use the `npm install` command to install the Azure Communication Services Calling SDK for JavaScript. ```console npm install @azure/communication-common --save npm install @azure/communication-calling --save ``` ### Set up the app framework This quickstart uses webpack to bundle the application assets. Run the following command to install the `webpack`, `webpack-cli` and `webpack-dev-server` npm packages and list them as development dependencies in your `package.json`: ```console npm install copy-webpack-plugin@^11.0.0 webpack@^5.88.2 webpack-cli@^5.1.4 webpack-dev-server@^4.15.1 --save-dev ``` Here's the code: Create an `index.html` file in the root directory of your project. We use this file to configure a basic layout that allows the user to place a 1:1 video call. ```html <!-- index.html --> <!DOCTYPE html> <html> <head> <title>Azure Communication Services - Calling Web SDK</title> <link rel="stylesheet" type="text/css" href="styles.css"/> </head> <body> <h4>Azure Communication Services - Calling Web SDK</h4> <input id="user-access-token" type="text" placeholder="User access token" style="margin-bottom:1em; width: 500px;"/> <button id="initialize-call-agent" type="button">Initialize Call Agent</button> <br> <br> <input id="callee-acs-user-id" type="text" placeholder="Enter callee's Azure Communication Services user identity in format: '8:acs:resourceId_userId'" style="margin-bottom:1em; width: 500px; display: block;"/> <button id="start-call-button" type="button" disabled="true">Start Call</button> <button id="hangup-call-button" type="button" disabled="true">Hang up Call</button> <button id="accept-call-button" type="button" disabled="true">Accept Call</button> <button id="start-video-button" type="button" disabled="true">Start Video</button> <button id="stop-video-button" type="button" disabled="true">Stop Video</button> <br> <br> <div id="connectedLabel" style="color: #13bb13;" hidden>Call is connected!</div> <br> <div id="remoteVideosGallery" style="width: 40%;" hidden>Remote participants' video streams:</div> <br> <div id="localVideoContainer" style="width: 30%;" hidden>Local video stream:</div> <!-- points to the bundle generated from client.js --> <script src="./main.js"></script> </body> </html> ``` 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. | | `AzureCommunicationTokenCredential` | Implements the `CommunicationTokenCredential` interface, which is used to instantiate `callAgent`. | | `CallAgent` | Used to start and manage calls. | | `DeviceManager` | Used to manage media devices. | | `Call` | Used for representing a Call | | `LocalVideoStream` | Used for creating a local video stream for a camera device on the local system. | | `RemoteParticipant` | Used for representing a remote participant in the Call | | `RemoteVideoStream` | Used for representing a remote video stream from a Remote Participant. Create a file in the root directory of your project called `index.js` to contain the application logic for this quickstart. Add the following code to index.js: ```JavaScript // Make sure to install the necessary dependencies const { CallClient, VideoStreamRenderer, LocalVideoStream } = require('@azure/communication-calling'); const { AzureCommunicationTokenCredential } = require('@azure/communication-common'); const { AzureLogger, setLogLevel } = require("@azure/logger"); // Set the log level and output setLogLevel('verbose'); AzureLogger.log = (...args) => { console.log(...args); }; // Calling web sdk objects let callAgent; let deviceManager; let call; let incomingCall; let localVideoStream; let localVideoStreamRenderer; // UI widgets let userAccessToken = document.getElementById('user-access-token'); let calleeAcsUserId = document.getElementById('callee-acs-user-id'); let initializeCallAgentButton = document.getElementById('initialize-call-agent'); let startCallButton = document.getElementById('start-call-button'); let hangUpCallButton = document.getElementById('hangup-call-button'); let acceptCallButton = document.getElementById('accept-call-button'); let startVideoButton = document.getElementById('start-video-button'); let stopVideoButton = document.getElementById('stop-video-button'); let connectedLabel = document.getElementById('connectedLabel'); let remoteVideosGallery = document.getElementById('remoteVideosGallery'); let localVideoContainer = document.getElementById('localVideoContainer'); /** * Using the CallClient, initialize a CallAgent instance with a CommunicationUserCredential which will enable us to make outgoing calls and receive incoming calls. * You can then use the CallClient.getDeviceManager() API instance to get the DeviceManager. */ initializeCallAgentButton.onclick = async () => { try { const callClient = new CallClient(); tokenCredential = new AzureCommunicationTokenCredential(userAccessToken.value.trim()); callAgent = await callClient.createCallAgent(tokenCredential) // Set up a camera device to use. deviceManager = await callClient.getDeviceManager(); await deviceManager.askDevicePermission({ video: true }); await deviceManager.askDevicePermission({ audio: true }); // Listen for an incoming call to accept. callAgent.on('incomingCall', async (args) => { try { incomingCall = args.incomingCall; acceptCallButton.disabled = false; startCallButton.disabled = true; } catch (error) { console.error(error); } }); startCallButton.disabled = false; initializeCallAgentButton.disabled = true; } catch(error) { console.error(error); } } /** * Place a 1:1 outgoing video call to a user * Add an event listener to initiate a call when the `startCallButton` is clicked: * First you have to enumerate local cameras using the deviceManager `getCameraList` API. * In this quickstart we're using the first camera in the collection. Once the desired camera is selected, a * LocalVideoStream instance will be constructed and passed within `videoOptions` as an item within the * localVideoStream array to the call method. Once your call connects it will automatically start sending a video stream to the other participant. */ startCallButton.onclick = async () => { try { const localVideoStream = await createLocalVideoStream(); const videoOptions = localVideoStream ? { localVideoStreams: [localVideoStream] } : undefined; call = callAgent.startCall([{ communicationUserId: calleeAcsUserId.value.trim() }], { videoOptions }); // Subscribe to the call's properties and events. subscribeToCall(call); } catch (error) { console.error(error); } } /** * Accepting an incoming call with video * Add an event listener to accept a call when the `acceptCallButton` is clicked: * After subscribing to the `CallAgent.on('incomingCall')` event, you can accept the incoming call. * You can pass the local video stream which you want to use to accept the call with. */ acceptCallButton.onclick = async () => { try { const localVideoStream = await createLocalVideoStream(); const videoOptions = localVideoStream ? { localVideoStreams: [localVideoStream] } : undefined; call = await incomingCall.accept({ videoOptions }); // Subscribe to the call's properties and events. subscribeToCall(call); } catch (error) { console.error(error); } } /** * Subscribe to a call obj. * Listen for property changes and collection updates. */ subscribeToCall = (call) => { try { // Inspect the initial call.id value. console.log(`Call Id: ${call.id}`); //Subscribe to call's 'idChanged' event for value changes. call.on('idChanged', () => { console.log(`Call Id changed: ${call.id}`); }); // Inspect the initial call.state value. console.log(`Call state: ${call.state}`); // Subscribe to call's 'stateChanged' event for value changes. call.on('stateChanged', async () => { console.log(`Call state changed: ${call.state}`); if(call.state === 'Connected') { connectedLabel.hidden = false; acceptCallButton.disabled = true; startCallButton.disabled = true; hangUpCallButton.disabled = false; startVideoButton.disabled = false; stopVideoButton.disabled = false; remoteVideosGallery.hidden = false; } else if (call.state === 'Disconnected') { connectedLabel.hidden = true; startCallButton.disabled = false; hangUpCallButton.disabled = true; startVideoButton.disabled = true; stopVideoButton.disabled = true; console.log(`Call ended, call end reason={code=${call.callEndReason.code}, subCode=${call.callEndReason.subCode}}`); } }); call.on('isLocalVideoStartedChanged', () => { console.log(`isLocalVideoStarted changed: ${call.isLocalVideoStarted}`); }); console.log(`isLocalVideoStarted: ${call.isLocalVideoStarted}`); call.localVideoStreams.forEach(async (lvs) => { localVideoStream = lvs; await displayLocalVideoStream(); }); call.on('localVideoStreamsUpdated', e => { e.added.forEach(async (lvs) => { localVideoStream = lvs; await displayLocalVideoStream(); }); e.removed.forEach(lvs => { removeLocalVideoStream(); }); }); // Inspect the call's current remote participants and subscribe to them. call.remoteParticipants.forEach(remoteParticipant => { subscribeToRemoteParticipant(remoteParticipant); }); // Subscribe to the call's 'remoteParticipantsUpdated' event to be // notified when new participants are added to the call or removed from the call. call.on('remoteParticipantsUpdated', e => { // Subscribe to new remote participants that are added to the call. e.added.forEach(remoteParticipant => { subscribeToRemoteParticipant(remoteParticipant) }); // Unsubscribe from participants that are removed from the call e.removed.forEach(remoteParticipant => { console.log('Remote participant removed from the call.'); }); }); } catch (error) { console.error(error); } } /** * Subscribe to a remote participant obj. * Listen for property changes and collection updates. */ subscribeToRemoteParticipant = (remoteParticipant) => { try { // Inspect the initial remoteParticipant.state value. console.log(`Remote participant state: ${remoteParticipant.state}`); // Subscribe to remoteParticipant's 'stateChanged' event for value changes. remoteParticipant.on('stateChanged', () => { console.log(`Remote participant state changed: ${remoteParticipant.state}`); }); // Inspect the remoteParticipants's current videoStreams and subscribe to them. remoteParticipant.videoStreams.forEach(remoteVideoStream => { subscribeToRemoteVideoStream(remoteVideoStream) }); // Subscribe to the remoteParticipant's 'videoStreamsUpdated' event to be // notified when the remoteParticipant adds new videoStreams and removes video streams. remoteParticipant.on('videoStreamsUpdated', e => { // Subscribe to new remote participant's video streams that were added. e.added.forEach(remoteVideoStream => { subscribeToRemoteVideoStream(remoteVideoStream) }); // Unsubscribe from remote participant's video streams that were removed. e.removed.forEach(remoteVideoStream => { console.log('Remote participant video stream was removed.'); }) }); } catch (error) { console.error(error); } } /** * Subscribe to a remote participant's remote video stream obj. * You have to subscribe to the 'isAvailableChanged' event to render the remoteVideoStream. If the 'isAvailable' property * changes to 'true', a remote participant is sending a stream. 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. */ subscribeToRemoteVideoStream = async (remoteVideoStream) => { let renderer = new VideoStreamRenderer(remoteVideoStream); let view; let remoteVideoContainer = document.createElement('div'); remoteVideoContainer.className = 'remote-video-container'; let loadingSpinner = document.createElement('div'); loadingSpinner.className = 'loading-spinner'; remoteVideoStream.on('isReceivingChanged', () => { try { if (remoteVideoStream.isAvailable) { const isReceiving = remoteVideoStream.isReceiving; const isLoadingSpinnerActive = remoteVideoContainer.contains(loadingSpinner); if (!isReceiving && !isLoadingSpinnerActive) { remoteVideoContainer.appendChild(loadingSpinner); } else if (isReceiving && isLoadingSpinnerActive) { remoteVideoContainer.removeChild(loadingSpinner); } } } catch (e) { console.error(e); } }); const createView = async () => { // Create a renderer view for the remote video stream. view = await renderer.createView(); // Attach the renderer view to the UI. remoteVideoContainer.appendChild(view.target); remoteVideosGallery.appendChild(remoteVideoContainer); } // Remote participant has switched video on/off remoteVideoStream.on('isAvailableChanged', async () => { try { if (remoteVideoStream.isAvailable) { await createView(); } else { view.dispose(); remoteVideosGallery.removeChild(remoteVideoContainer); } } catch (e) { console.error(e); } }); // Remote participant has video on initially. if (remoteVideoStream.isAvailable) { try { await createView(); } catch (e) { console.error(e); } } } /** * Start your local video stream. * This will send your local video stream to remote participants so they can view it. */ startVideoButton.onclick = async () => { try { const localVideoStream = await createLocalVideoStream(); await call.startVideo(localVideoStream); } catch (error) { console.error(error); } } /** * Stop your local video stream. * This will stop your local video stream from being sent to remote participants. */ stopVideoButton.onclick = async () => { try { await call.stopVideo(localVideoStream); } catch (error) { console.error(error); } } /** * To render a LocalVideoStream, you need to 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. */ createLocalVideoStream = async () => { const camera = (await deviceManager.getCameras())[0]; if (camera) { return new LocalVideoStream(camera); } else { console.error(`No camera device found on the system`); } } /** * Display your local video stream preview in your UI */ displayLocalVideoStream = async () => { try { localVideoStreamRenderer = new VideoStreamRenderer(localVideoStream); const view = await localVideoStreamRenderer.createView(); localVideoContainer.hidden = false; localVideoContainer.appendChild(view.target); } catch (error) { console.error(error); } } /** * Remove your local video stream preview from your UI */ removeLocalVideoStream = async() => { try { localVideoStreamRenderer.dispose(); localVideoContainer.hidden = true; } catch (error) { console.error(error); } } /** * End current call */ hangUpCallButton.addEventListener("click", async () => { // end the current call await call.hangUp(); }); ``` Create a file in the root directory of your project called `styles.css` to contain the application styling for this quickstart. Add the following code to styles.css: ```css /** * CSS for styling the loading spinner over the remote video stream */ .remote-video-container { position: relative; } .loading-spinner { border: 12px solid #f3f3f3; border-radius: 50%; border-top: 12px solid #ca5010; width: 100px; height: 100px; -webkit-animation: spin 2s linear infinite; /* Safari */ animation: spin 2s linear infinite; position: absolute; margin: auto; top: 0; bottom: 0; left: 0; right: 0; transform: translate(-50%, -50%); } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } /* Safari */ @-webkit-keyframes spin { 0% { -webkit-transform: rotate(0deg); } 100% { -webkit-transform: rotate(360deg); } } ``` ## Add the webpack local server code Create a file in the root directory of your project called **webpack.config.js** to contain the local server logic for this quickstart. Add the following code to **webpack.config.js**: ```javascript const path = require('path'); const CopyPlugin = require("copy-webpack-plugin"); module.exports = { mode: 'development', entry: './index.js', output: { filename: 'main.js', path: path.resolve(__dirname, 'dist'), }, devServer: { static: { directory: path.join(__dirname, './') }, }, plugins: [ new CopyPlugin({ patterns: [ './index.html' ] }), ] }; ``` ## Run the code Use the `webpack-dev-server` to build and run your app. Run the following command to bundle the application host in a local webserver: ```console npx webpack serve --config webpack.config.js ``` Open your browser and on two tabs navigate to http://localhost:8080/.You should see the following screen: :::image type="content" source="../../media/javascript/1-on-1-video-calling-a.png" alt-text="1 on 1 video calling page - a"::: On the first tab, enter a valid user access token, and on the other tab enter another different valid user access token. Refer to the [user access token documentation](../../../identity/access-tokens.md), if you don't already have tokens available to use. On both tabs, click on the "Initialize Call Agent" buttons. You should see the following screen: :::image type="content" source="../../media/javascript/1-on-1-video-calling-b.png" alt-text="1 on 1 video calling page - b"::: On the first tab, enter the Azure Communication Services user identity of the second tab, and click the "Start Call" button. The first tab starts the outgoing call to the second tab, and the second tab's "Accept Call" button becomes enabled: :::image type="content" source="../../media/javascript/1-on-1-video-calling-c.png" alt-text="1 on 1 video calling page - c"::: From the second tab, click on the "Accept Call" button and the call starts and connect. You should see the following screen: :::image type="content" source="../../media/javascript/1-on-1-video-calling-d.png" alt-text="1 on 1 video calling page - d"::: Both tabs are now successfully in a 1 to 1 video call. Both tabs can hear each other's audio and see each other video stream.
Success! Branch created successfully. Create Pull Request on GitHub
Error: