Guida introduttiva: Aggiungere l'app chiamante a una coda di chiamate di Teams

In questa guida introduttiva si apprenderà come avviare una chiamata da Servizi di comunicazione di Azure utente alla coda di chiamate di Teams. L'operazione verrà ottenuta seguendo questa procedura:

  1. Abilitare la federazione di Servizi di comunicazione di Azure risorsa con il tenant di Teams.
  2. Selezionare o creare una coda di chiamate di Teams tramite Teams Amministrazione Center.
  3. Ottenere l'indirizzo di posta elettronica della coda di chiamate tramite Teams Amministrazione Center.
  4. Ottenere l'ID oggetto della coda di chiamate tramite l'API Graph.
  5. Avviare una chiamata con Servizi di comunicazione di Azure Calling SDK.

Se si vuole passare direttamente alla fine, è possibile scaricare questa guida introduttiva come esempio in GitHub.

Abilitare l'interoperabilità nel tenant di Teams

L'utente Di Microsoft Entra con il ruolo di amministratore di Teams può eseguire il cmdlet di PowerShell con il modulo MicrosoftTeams per abilitare la risorsa servizi di comunicazione nel tenant.

1. Preparare il modulo di Microsoft Teams

Prima di tutto, aprire PowerShell e convalidare l'esistenza del modulo Teams con il comando seguente:

Get-module *teams* 

Se il modulo non viene visualizzato MicrosoftTeams , installarlo per primo. Per installare il modulo, è necessario eseguire PowerShell come amministratore. Poi eseguire quindi il comando seguente.

	Install-Module -Name MicrosoftTeams

Verranno informati sui moduli che verranno installati, che è possibile confermare con una Y risposta o A . Se il modulo è installato ma non aggiornato, è possibile eseguire il comando seguente per aggiornare il modulo:

	Update-Module MicrosoftTeams

2. Connessione al modulo di Microsoft Teams

Quando il modulo è installato e pronto, è possibile connettersi al modulo MicrosftTeams con il comando seguente. Verrà visualizzata una finestra interattiva per l'accesso. L'account utente che si intende usare deve avere le autorizzazioni di amministratore di Teams. In caso contrario, è possibile ottenere una access denied risposta nei passaggi successivi.

Connect-MicrosoftTeams

3. Abilitare la configurazione del tenant

L'interoperabilità con le risorse di Servizi di comunicazione viene controllata tramite la configurazione del tenant e i criteri assegnati. Il tenant di Teams ha una configurazione a tenant singolo e gli utenti di Teams hanno assegnato criteri globali o criteri personalizzati. Per altre informazioni, vedere Assegnare criteri in Teams.

Al termine dell'accesso, è possibile eseguire il cmdlet Set-CsTeamsAcsFederationConfiguration per abilitare la risorsa servizi di comunicazione nel tenant. Sostituire il testo IMMUTABLE_RESOURCE_ID con un ID risorsa non modificabile nella risorsa di comunicazione. Per altri dettagli su come ottenere queste informazioni , vedere qui.

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

4. Abilitare i criteri del tenant

A ogni utente di Teams è stato assegnato un oggetto External Access Policy che determina se gli utenti di Servizi di comunicazione possono chiamare l'utente di Teams. Usare il cmdlet Set-CsExternalAccessPolicy per assicurarsi che i criteri assegnati all'utente di Teams siano impostati su EnableAcsFederationAccess$true

Set-CsExternalAccessPolicy -Identity Global -EnableAcsFederationAccess $true

Creare o selezionare Coda chiamate di Teams

La coda di chiamate di Teams è una funzionalità di Microsoft Teams che distribuisce in modo efficiente le chiamate in ingresso tra un gruppo di utenti o agenti designati. È utile per gli scenari di assistenza clienti o call center. Le chiamate vengono inserite in una coda e assegnate all'agente disponibile successivo in base a un metodo di routing predeterminato. Gli agenti ricevono notifiche e possono gestire le chiamate usando i controlli delle chiamate di Teams. La funzionalità offre report e analisi per il rilevamento delle prestazioni. Semplifica la gestione delle chiamate, garantisce un'esperienza cliente coerente e ottimizza la produttività degli agenti. È possibile selezionare una nuova coda di chiamate o crearne una nuova tramite Teams Amministrazione Center.

Altre informazioni su come creare una coda di chiamate usando Teams Amministrazione Center qui.

Trovare l'ID oggetto per la coda di chiamate

Dopo aver creato la coda delle chiamate, è necessario trovare l'ID oggetto correlato per usarlo in un secondo momento per le chiamate. L'ID oggetto è connesso all'account risorsa collegato alla coda delle chiamate. Aprire la scheda Account risorsa in Teams Amministrazione e trovare il messaggio di posta elettronica. Screenshot degli account delle risorse nel portale di Teams Amministrazione. Tutte le informazioni necessarie per l'account di risorsa sono disponibili in Microsoft Graph Explorer usando questo messaggio di posta elettronica nella ricerca.

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

Nei risultati sarà possibile trovare il campo "ID"

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

Prerequisiti

Configurazione

Creare una nuova applicazione Node.js

Aprire il terminale o la finestra di comando per creare una nuova directory per l'app e passare alla directory.

mkdir calling-quickstart && cd calling-quickstart

Installare il pacchetto

Usare il npm install comando per installare Servizi di comunicazione di Azure Calling SDK per JavaScript.

Importante

Questa guida introduttiva usa la versione nextServizi di comunicazione di Azure Calling SDK .

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

Configurare il framework dell'app

Questa guida di avvio rapido usa webpack per aggregare gli asset dell'applicazione. Eseguire il comando seguente per installare i webpackpacchetti , webpack-cli e webpack-dev-server npm ed elencarli come dipendenze di sviluppo in 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

Creare un index.html file nella directory radice del progetto. Questo file verrà usato per configurare un layout di base che consentirà all'utente di inserire una videochiamata 1:1.

Ecco il codice:

<!-- 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 callee's Teams user 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>

Servizi di comunicazione di Azure Modello a oggetti di Web SDK per chiamate

Le classi e le interfacce seguenti gestiscono alcune delle funzionalità principali di Servizi di comunicazione di Azure Calling SDK:

Nome Descrizione
CallClient Punto di ingresso principale di Calling SDK.
CallAgent Usato per avviare e gestire le chiamate.
DeviceManager Usato per gestire i dispositivi multimediali.
Call Utilizzato per rappresentare una chiamata.
LocalVideoStream Usato per creare un flusso video locale per un dispositivo fotocamera nel sistema locale.
RemoteParticipant Utilizzato per rappresentare un partecipante remoto nella chiamata.
RemoteVideoStream Utilizzato per rappresentare un flusso video remoto da un partecipante remoto.

Creare un file nella directory radice del progetto chiamato client.js per contenere la logica dell'applicazione per questa guida introduttiva. Aggiungere il codice seguente a 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 callQueueId = 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 a Teams Call Queue
 * 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: callQueueId.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.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();
            });
        });
        
        call.on('isLocalVideoStartedChanged', () => {
            console.log(`isLocalVideoStarted changed: ${call.isLocalVideoStarted}`);
        });
        console.log(`isLocalVideoStarted: ${call.isLocalVideoStarted}`);
        // 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();
});

Aggiungere il codice del server locale webpack

Creare un file nella directory radice del progetto denominato webpack.config.js per contenere la logica del server locale per questa guida introduttiva. Aggiungere il codice seguente a 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'
            ]
        }),
    ]
};

Eseguire il codice

Usare webpack-dev-server per compilare ed eseguire l'app. Eseguire il comando seguente per aggregare l'host dell'applicazione in un server Web locale:

npx webpack serve --config webpack.config.js

Passaggi manuali per configurare la chiamata:

  1. Aprire il browser e passare a http://localhost:8080/.
  2. Immettere un token di accesso utente valido. Fare riferimento alla documentazione del token di accesso utente se non si dispone già di token di accesso disponibili per l'uso.
  3. Fare clic sui pulsanti "Inizializza agente di chiamata".
  4. Immettere l'ID oggetto coda chiamate e selezionare il pulsante "Avvia chiamata". L'applicazione avvierà la chiamata in uscita alla coda di chiamate con l'ID oggetto specificato.
  5. La chiamata è connessa alla coda delle chiamate.
  6. L'utente di Servizi di comunicazione viene instradato tramite coda di chiamate in base alla configurazione.

Pulire le risorse

Se si vuole pulire e rimuovere una sottoscrizione a Servizi di comunicazione, è possibile eliminare la risorsa o il gruppo di risorse. L'eliminazione del gruppo di risorse comporta anche l'eliminazione di tutte le altre risorse associate. Altre informazioni sulla pulizia delle risorse.

Passaggi successivi

Per altre informazioni, vedere gli articoli seguenti: