Compartilhar via


Guia de início rápido: juntar seu aplicativo de chamadas a um Atendedor Automático do Teams

Neste início rápido, você aprenderá como iniciar uma chamada do usuário dos Serviços de Comunicação do Azure para o Atendedor Automático do Teams. Você conseguirá fazer isso através das seguintes etapas:

  1. Habilite a federação do recurso de Serviços de Comunicação do Azure com o Locatário do Teams.
  2. Selecione ou crie o Atendedor Automático do Teams por meio do Centro de Administração do Teams.
  3. Obtenha o endereço de email do Atendedor Automático por meio do Centro de Administração do Teams.
  4. Obtenha a ID do Objeto do Atendedor Automático por meio da API do Graph.
  5. Iniciar uma chamada com o SDK de Chamada dos Serviços de Comunicação do Azure.

Para pular para o final, baixe este guia de início rápido como um exemplo no GitHub.

Habilitar a interoperabilidade em seu locatário do Teams

O usuário do Microsoft Entra com função de administrador do Teams pode executar o cmdlet do PowerShell com o módulo MicrosoftTeams para habilitar o recurso Serviços de Comunicação no locatário.

1. Preparar o módulo do Microsoft Teams

Primeiro, abra o PowerShell e valide a existência do módulo do Teams com o seguinte comando:

Get-module *teams* 

Se você não vir o módulo MicrosoftTeams, instale-o primeiro. Para instalar o módulo, você precisa executar o PowerShell como administrador. Em seguida, execute o seguinte comando:

	Install-Module -Name MicrosoftTeams

Você será informado sobre os módulos que serão instalados, que podem ser confirmados com uma resposta Y ou A. Se o módulo estiver instalado, mas estiver desatualizado, você poderá executar o seguinte comando para atualizar o módulo:

	Update-Module MicrosoftTeams

2. Conectar-se ao módulo do Microsoft Teams

Quando o módulo estiver instalado e pronto, você poderá se conectar ao módulo MicrosftTeams com o comando a seguir. Você receberá uma solicitação com uma janela interativa para fazer logon. A conta de usuário que você usará precisa ter permissões de administrador do Teams. Caso contrário, você poderá obter uma resposta access denied nas próximas etapas.

Connect-MicrosoftTeams

3. Habilitar a configuração do locatário

A interoperabilidade com recursos dos Serviços de Comunicação é controlada por meio da configuração do locatário e da política atribuída. O locatário do Teams tem uma única configuração de locatário e os usuários do Teams têm atribuída a política global ou política personalizada. Para obter mais informações, consulte Políticas de atribuição no Teams.

Após o logon bem-sucedido, você pode executar o cmdlet Set-CsTeamsAcsFederationConfiguration para habilitar o recurso dos Serviços de Comunicação em seu locatário. Substitua o texto IMMUTABLE_RESOURCE_ID por uma ID de recurso imutável no recurso de comunicação. Você pode encontrar mais detalhes sobre como obter essas informações aqui.

$allowlist = @('IMMUTABLE_RESOURCE_ID')
Set-CsTeamsAcsFederationConfiguration -EnableAcsUsers $True -AllowedAcsResources $allowlist

4. Habilitar a política de locatário

Cada usuário do Teams atribuiu um External Access Policy que determina se os usuários dos Serviços de Comunicação podem chamar esse usuário do Teams. Use o cmdlet Set-CsExternalAccessPolicy para garantir que a política atribuída ao usuário do Teams tenha EnableAcsFederationAccess definido como $true

Set-CsExternalAccessPolicy -Identity Global -EnableAcsFederationAccess $true

Criar ou selecionar o Atendedor Automático do Teams

O Atendedor Automático do Teams é um sistema que fornece um sistema automatizado de tratamento de chamadas para chamadas recebidas. Ele serve como recepcionista virtual, permitindo que a pessoa que está ligando seja automaticamente encaminhada para a pessoa ou departamento apropriado sem a necessidade de um operador humano. Você pode selecionar a Fila de Chamadas existente ou criar um novo Atendedor Automático através do Centro de Administração do Teams.

Saiba mais sobre como criar o Atendedor Automático usando o Centro de Administração do Teams aqui.

Localizar a ID de Objeto do Atendedor Automático

Depois que o Atendedor Automático é criado, precisamos encontrar a ID de Objeto correlacionada para usá-la posteriormente nas chamadas. A ID do objeto está conectada à conta de recursos que foi anexada ao Atendedor Automático. Abra a guia Contas de Recursos no Administrador do Teams e encontre o email da conta. Captura de tela das Contas de Recursos no Portal de Administração do Teams. Todas as informações necessárias para a Conta de Recursos podem ser encontradas no Microsoft Graph Explorer usando esse email na pesquisa.

https://graph.microsoft.com/v1.0/users/lab-test2-cq-@contoso.com

Nos resultados, poderemos encontrar o campo "ID"

    "userPrincipalName": "lab-test2-cq@contoso.com",
    "id": "31a011c2-2672-4dd0-b6f9-9334ef4999db"

Pré-requisitos

Configurando

Criar um novo aplicativo do Node.js

Abra o terminal ou a janela de comando Criar um diretório para seu aplicativo e navegue até o diretório.

mkdir calling-quickstart && cd calling-quickstart

Instalar o pacote

Use o comando npm install para instalar o SDK de Chamada dos Serviços de Comunicação do Azure para JavaScript.

Importante

Este guia de início rápido usa a versão next do SDK da Chamada dos Serviços de Comunicação do Azure.

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

Configurar o framework de aplicativos

Este guia de início rápido usa o webpack para agrupar os ativos do aplicativo. Execute o seguinte comando para instalar os pacotes npm webpack, webpack-cli e webpack-dev-server e listá-los como dependências de desenvolvimento no package.json:

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

Crie um arquivo index.html no diretório raiz do projeto. Usaremos esse arquivo para configurar um layout básico que permitirá que o usuário faça uma chamada de vídeo 1:1.

O código é o seguinte:

<!-- index.html -->
<!DOCTYPE html>
<html>
    <head>
        <title>Azure Communication Services - Calling Web SDK</title>
    </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-teams-call-agent" type="button">Initialize Call Agent</button>
        <br>
        <br>
        <input id="application-object-id"
            type="text"
            placeholder="Enter application objectId identity in format: 'APP_GUID'"
            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="remoteVideoContainer" 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>

Modelo de objeto do SDK Web de chamada de Serviços de Comunicação do Azure

As seguintes classes e interfaces cuidam de alguns dos principais recursos do SDK de Chamada dos Serviços de Comunicação do Azure:

Nome Descrição
CallClient O ponto de entrada principal para o SDK de Chamada.
CallAgent Usado para iniciar e gerenciar chamadas.
DeviceManager Usado para gerenciar dispositivos de mídia.
Call Usado para representar uma Chamada.
LocalVideoStream Usado para criar um fluxo de vídeo local para um dispositivo de câmera no sistema local.
RemoteParticipant Usado para representar um participante remoto da Chamada.
RemoteVideoStream Usado para representar um fluxo de vídeo remoto de um Participante Remoto.

Crie um arquivo no diretório raiz do projeto chamado client.js para conter a lógica do aplicativo deste guia de início rápido. Adicione o seguinte código ao client.js:

// 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 applicationObjectId = document.getElementById('application-object-id');
let initializeCallAgentButton = document.getElementById('initialize-teams-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 remoteVideoContainer = document.getElementById('remoteVideoContainer');
let localVideoContainer = document.getElementById('localVideoContainer');
/**
 * Create an instance of CallClient. Initialize a CallAgent instance with a AzureCommunicationTokenCredential via created CallClient. CallAgent enables 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 an Teams Auto attendant
 * Add an event listener to initiate a call when the `startCallButton` is selected.
 * 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. When the call connects, your application will be 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([{ teamsAppId: applicationObjectId.value.trim(), cloud:"public" }], { videoOptions: videoOptions });
        // Subscribe to the call's properties and events.
        subscribeToCall(call);
    } catch (error) {
        console.error(error);
    }
}
/**
 * Accepting an incoming call with a video
 * Add an event listener to accept a call when the `acceptCallButton` is selected.
 * You can accept incoming calls after subscribing to the `CallAgent.on('incomingCall')` event.
 * You can pass the local video stream to accept the call with the following code.
 */
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 udpates.
subscribeToCall = (call) => {
    try {
        // Inspect the initial call.id value.
        console.log(`Call Id: ${call.id}`);
        //Subsribe 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;
            } 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 udpates.
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 remoteParticiapant adds new videoStreams and removes video streams.
        remoteParticipant.on('videoStreamsUpdated', e => {
            // Subscribe to newly added remote participant's video streams.
            e.added.forEach(remoteVideoStream => {
                subscribeToRemoteVideoStream(remoteVideoStream)
            });
            // Unsubscribe from newly removed remote participants' video streams.
            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 the availability of a remote stream changes
 * you can choose to destroy the whole 'Renderer' a specific 'RendererView' or keep them. Displaying RendererView without a video stream will result in a blank video frame. 
 */
subscribeToRemoteVideoStream = async (remoteVideoStream) => {
    // Create a video stream renderer for the remote video stream.
    let videoStreamRenderer = new VideoStreamRenderer(remoteVideoStream);
    let view;
    const renderVideo = async () => {
        try {
            // Create a renderer view for the remote video stream.
            view = await videoStreamRenderer.createView();
            // Attach the renderer view to the UI.
            remoteVideoContainer.hidden = false;
            remoteVideoContainer.appendChild(view.target);
        } catch (e) {
            console.warn(`Failed to createView, reason=${e.message}, code=${e.code}`);
        }	
    }
    
    remoteVideoStream.on('isAvailableChanged', async () => {
        // Participant has switched video on.
        if (remoteVideoStream.isAvailable) {
            await renderVideo();
        // Participant has switched video off.
        } else {
            if (view) {
                view.dispose();
                view = undefined;
            }
        }
    });
    // Participant has video on initially.
    if (remoteVideoStream.isAvailable) {
        await renderVideo();
    }
}
// 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. 
 */
// Create a local video stream for your camera device
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 the current call
hangUpCallButton.addEventListener("click", async () => {
    // end the current call
    await call.hangUp();
});

Adicionar o código do servidor local do webpack

Crie um arquivo no diretório-raiz do projeto chamado webpack.config.js para conter a lógica do servidor local deste início rápido. Adicione o seguinte código ao webpack.config.js:

const path = require('path');
const CopyPlugin = require("copy-webpack-plugin");

module.exports = {
    mode: 'development',
    entry: './client.js',
    output: {
        filename: 'main.js',
        path: path.resolve(__dirname, 'dist'),
    },
    devServer: {
        static: {
            directory: path.join(__dirname, './')
        },
    },
    plugins: [
        new CopyPlugin({
            patterns: [
                './index.html'
            ]
        }),
    ]
};

Executar o código

Use webpack-dev-server para criar e executar o seu aplicativo. Execute o seguinte comando para empacotar o host de aplicativos em um servidor Web local:

npx webpack serve --config webpack.config.js

Etapas manuais para configurar a chamada:

  1. Abra o navegador e navegue até http://localhost:8080/.
  2. Digite um token de acesso do usuário válido. Confira a documentação do token de acesso do usuário se você ainda não tem tokens de acesso disponíveis para uso.
  3. Clique nos botões "Inicializar o Agente de Chamadas".
  4. Insira a ID de Objeto do Atendedor Automático e selecione o botão "Iniciar chamada". O aplicativo iniciará a chamada de saída para o atendedor automático com a ID de Objeto fornecida.
  5. A chamada está conectada ao Atendedor Automático.
  6. O usuário dos Serviços de Comunicação é roteado por meio do Atendedor Automático com base nas configurações.

Limpar os recursos

Se quiser limpar e remover uma assinatura dos Serviços de Comunicação, exclua o recurso ou o grupo de recursos. Excluir o grupo de recursos também exclui todos os recursos associados a ele. Saiba mais sobre como limpar recursos.

Próximas etapas

Para obter mais informações, consulte os seguintes artigos: