> This is a page from the ElevenLabs documentation. For a complete page index, fetch https://elevenlabs.io/docs/llms.txt. For the full documentation in a single file, fetch https://elevenlabs.io/docs/llms-full.txt.

# Bot d’appel Graph

## Vue d’ensemble

Cette approche fait de l’agent une **identité Teams appelable**. Un utilisateur le recherche par son nom et l’appelle en tête-à-tête, l’agent répond en temps réel, sans numéro de téléphone, RTPC ni crédits de communication. C’est la seule approche appelable par son nom et la plus complexe à mettre en œuvre.

Elle utilise un **bot de médias en temps réel Microsoft Graph** (la plateforme d’appel Cloud Communications). Le SDK média (`Microsoft.Skype.Bots.Media`) fonctionne **uniquement avec .NET sur Windows Server**, il n’existe aucune option Linux ou autre que .NET pour l’audio brut lors des appels Teams.

> **Note**
>
> C’est la seule approche appelable **par son nom** dans Teams. Préférez l’[onglet widget](/docs/fr/eleven-agents/phone-numbers/microsoft-teams/widget-tab) pour une configuration plus légère, ou
> [ACS](/docs/fr/eleven-agents/phone-numbers/microsoft-teams/azure-communication-services) si vous
> souhaitez spécifiquement un numéro de téléphone.

## Fonctionnement

![Un utilisateur Teams appelle le bot par son nom ; Teams achemine l’appel vers le bot média sur une VM Windows, qui transmet l’audio PCM brut à 16 kHz à l’agent ElevenLabs via un WebSocket](/docs/_fern-files/elevenlabs.docs.buildwithfern.com/f411ec0b44d4c5ca1a61598fbd1e6162e46a1c74179eebb4b673ee1428823e84/assets/images/conversational-ai/teams-media-bot-architecture.svg)

Le bot répond avec des **médias hébergés par l’application**, reçoit 50 trames audio par seconde (PCM 16 kHz de 20 ms), les transmet à l’agent ElevenLabs via un WebSocket et diffuse l’audio de l’agent dans l’appel.

## Prérequis

1. Un enregistrement **Azure Bot** et une application (enregistrement d’application Entra).
2. Des **autorisations d’application Graph** avec consentement administrateur : `Calls.AccessMedia.All` (médias bruts) et `Calls.Initiate.All`.
3. Une **VM Windows Server** (≥ **2 cœurs physiques**, par exemple `Standard_D4s_v3`) avec une IP publique et des ports média ouverts.
4. Un **certificat TLS signé par une AC** sur un FQDN public pour le point de terminaison média/signalisation (la plateforme média refuse les certificats autosignés).
5. Un [agent ElevenLabs](/docs/fr/eleven-agents/quickstart) configuré sur **PCM 16000 Hz** dans les deux sens : le format de sortie TTS dans l’onglet **Voice**, le format audio d’entrée utilisateur dans l’onglet **Advanced**.

> **Warning**
>
> Une `D2s_v3` (2 vCPU = **1 cœur physique**) échoue avec `MediaPlatform needs a system with at least 2
>   cores`. Utilisez une taille avec ≥ 2 cœurs physiques (par exemple, `D4s_v3`).

## Autorisations et rôles

| Portée                        | Rôle / autorisation                                                                                                                                     | Pourquoi                                                                                                                                                              |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Entra                         | **Administrateur d’application**                                                                                                                        | créer l’enregistrement de l’application et l’Azure Bot                                                                                                                |
| Entra                         | **Administrateur général / Administrateur de rôle privilégié**                                                                                          | accorder le consentement administrateur pour les autorisations d’appel Graph, les autorisations d’application ne peuvent pas faire l’objet d’un consentement autonome |
| Microsoft Graph (application) | `Calls.AccessMedia.All`, `Calls.Initiate.All`                                                                                                           | répondre aux appels en tête-à-tête et accéder aux médias bruts                                                                                                        |
| Azure RBAC                    | **Contributeur** sur le groupe de ressources                                                                                                            | créer la VM Windows et l’Azure Bot                                                                                                                                    |
| Administrateur Teams          | autoriser le [**chargement d’application personnalisée**](https://admin.teams.microsoft.com/policies/manage-apps) ; activer le canal **Calling** du bot | charger l’application de façon indépendante et recevoir des appels                                                                                                    |

## Étape 1 : enregistrer le bot et les autorisations Graph

Créez un enregistrement d’application et un Azure Bot qui y est associé, puis accordez et approuvez les autorisations d’appel (vous devez être administrateur général ou administrateur de rôle privilégié pour donner votre consentement) :

```bash
APPID=$(az ad app create --display-name "ElevenLabs Teams Agent" \
  --sign-in-audience AzureADMyOrg --query appId -o tsv)
az ad sp create --id "$APPID"
# create a client secret and record it
az ad app credential reset --id "$APPID" --display-name bot --query password -o tsv

# Azure Bot bound to the app
az bot create --resource-group $RG --name el-teams-agent-bot \
  --app-type SingleTenant --appid "$APPID" --tenant-id $TENANT \
  --endpoint "https://YOUR_FQDN/api/messages" --sku S1
```

Accordez les deux rôles d’application Graph et le **consentement administrateur** (nécessite le rôle d’administrateur général ou d’administrateur de rôle privilégié), puis confirmez que les attributions ont bien été effectuées :

```bash
# Graph app roles: Calls.AccessMedia.All, Calls.Initiate.All
az ad app permission add --id "$APPID" --api 00000003-0000-0000-c000-000000000000 \
  --api-permissions a7a681dc-756e-4909-b988-f160edc6655f=Role \
    284383ee-7f6e-4e40-a2a8-e85dcb029101=Role
az ad app permission admin-consent --id "$APPID"

# Verify — should print both role ids
az rest --method GET \
  --url "https://graph.microsoft.com/v1.0/servicePrincipals(appId='$APPID')/appRoleAssignments" \
  --query "value[].appRoleId" -o tsv
```

> **Note**
>
> Si `admin-consent` renvoie `Consent validation failed`, accordez plutôt directement les rôles d’application sur le principal de service :
>
> ```bash
> GRAPH_SP=$(az ad sp show --id 00000003-0000-0000-c000-000000000000 --query id -o tsv)
> BOT_SP=$(az ad sp show --id "$APPID" --query id -o tsv)
> for ROLE in a7a681dc-756e-4909-b988-f160edc6655f 284383ee-7f6e-4e40-a2a8-e85dcb029101; do
>   az rest --method POST \
>     --url "https://graph.microsoft.com/v1.0/servicePrincipals/$GRAPH_SP/appRoleAssignedTo" \
>     --body "{\"principalId\": \"$BOT_SP\", \"resourceId\": \"$GRAPH_SP\", \"appRoleId\": \"$ROLE\"}"
> done
> ```

Dans le portail, vérifiez dans le [centre d’administration Entra](https://entra.microsoft.com/#view/Microsoft_AAD_RegisteredApps/RegisteredAppsListBlade), sous **App registrations → your app → API permissions** : les deux autorisations doivent afficher **Granted** avec des coches vertes.

![Le volet API permissions de l’enregistrement d’application indiquant que Calls.AccessMedia.All et Calls.Initiate.All sont
accordées](/docs/_fern-img/4abc1e220d3fc03845594aa4f835656425cc1c39448dae30dace98e672fd8fb1.webp)

## Étape 2 : provisionner la VM Windows, le certificat et les ports

```bash
az vm create -g $RG -n teams-media-bot --image Win2022Datacenter \
  --size Standard_D4s_v3 --admin-username azureuser --admin-password '<strong-pw>' \
  --public-ip-sku Standard --public-ip-address-dns-name elevenmediabot
az vm open-port -g $RG -n teams-media-bot --port 80,443,8445,9441 --priority 300
```

Sur la VM (le code natif de la plateforme média les nécessite, Windows Server ne les inclut pas par défaut) :

```powershell
# VC++ runtime + Media Foundation feature (required by NativeMedia.dll)
choco install -y vcredist140
Install-WindowsFeature Server-Media-Foundation

# CA cert for the VM's FQDN via win-acme (HTTP-01), then import to LocalMachine\My
& wacs.exe --target manual --host <vm-fqdn>.cloudapp.azure.com `
  --validation selfhosting --store pfxfile --pfxfilepath C:\bot\certs --accepttos
```

Ouvrez les mêmes ports dans le **pare-feu Windows** et notez l’**empreinte** du certificat, le bot y associe Kestrel (443 et un port de notifications) ainsi que la plateforme média (8445).

> **Tip**
>
> Le FQDN `*.cloudapp.azure.com` de la VM fonctionne avec un certificat Let's Encrypt, aucun domaine distinct
> n’est nécessaire.

## Étape 3 : créer et exécuter le bot

Partez de l’**PublicSamples/EchoBot** de [`microsoft-graph-comms-samples`](https://github.com/microsoftgraph/microsoft-graph-comms-samples), il cible `net6.0` et se compile avec le SDK .NET (aucun outil de génération Visual Studio requis) :

```powershell
git clone --depth 1 https://github.com/microsoftgraph/microsoft-graph-comms-samples.git C:\bot\samples
cd C:\bot\samples\Samples\PublicSamples\EchoBot\src
dotnet build EchoBot.sln -c Release
```

Configurez la section `AppSettings` de `appsettings.json` avec vos valeurs `AadAppId`, `AadAppSecret`, `ServiceDnsName`/`MediaDnsName` (le FQDN de la VM), `CertificateThumbprint` et les ports (`443` pour les appels, `9441` pour les notifications, `8445` pour les médias). Ajoutez deux paramètres pour le pont ElevenLabs ci-dessous : `ElevenLabsAgentId` et `ElevenLabsOrigin` (`wss://api.elevenlabs.io`, ou votre hôte de résidence des données). Exécutez-le en tant que tâche planifiée ou service Windows afin qu’il continue de fonctionner après les redémarrages.

> **Warning**
>
> La **limite de durée d’exécution par défaut (72 heures)** du Planificateur de tâches arrête silencieusement les tâches de longue durée, un bot lancé au démarrage s’arrête trois jours plus tard et les appels échouent avec « we couldn't connect you ». Désactivez la limite et ajoutez un redémarrage en cas d’échec :
>
> ```powershell
> $s = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Seconds 0) `
>   -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1) -StartWhenAvailable
> Set-ScheduledTask -TaskName EchoBot -Settings $s
> ```

> **Warning**
>
> L’EchoBot fourni s’arrête lorsqu’un appel est passé au port standard 443 : `HttpHelpers.SetAbsoluteUri`
> appelle `req.Host.Port.Value`, qui est nul lorsque l’en-tête Host n’a pas de port explicite. Corrigez-le avec
> `req.Host.Port ?? (req.IsHttps ? 443 : 80)`.

### Remplacer l’écho par ElevenLabs

L’interface audio d’EchoBot est claire : `SpeechService.AppendAudioBuffer(in)` et un événement `OnSendMediaBufferEventArgs(out)`. Remplacez son corps Azure Speech par un pont WebSocket d’agent ElevenLabs qui conserve la même interface :

**`SpeechService.cs : pont ElevenLabs (noyau)`**

```csharp title="SpeechService.cs : pont ElevenLabs (noyau)"
public class SpeechService
{
    private readonly AppSettings _settings;
    private readonly ILogger _logger;
    private ClientWebSocket _ws;
    private bool _started;
    private bool _connecting;

    public event EventHandler<MediaStreamEventArgs> SendMediaBuffer; // agent audio -> call
    public event EventHandler FlushMedia;                            // barge-in: drop queued audio

    public SpeechService(AppSettings settings, ILogger logger) { _settings = settings; _logger = logger; }

    // Caller audio -> ElevenLabs
    public async Task AppendAudioBuffer(AudioMediaBuffer buffer)
    {
        if (!_started)
        {
            if (_connecting) return;            // a connect attempt is already in flight
            _connecting = true;
            try { await Connect(); _started = true; }
            catch (Exception ex) { _logger.Error(ex, "ElevenLabs connect failed; retry on next frame"); return; }
            finally { _connecting = false; }
        }
        if (_ws?.State != WebSocketState.Open || buffer.Length <= 0) return;
        var pcm = new byte[buffer.Length];
        Marshal.Copy(buffer.Data, pcm, 0, (int)buffer.Length);
        var msg = JsonSerializer.Serialize(new { user_audio_chunk = Convert.ToBase64String(pcm) });
        await _ws.SendAsync(Encoding.UTF8.GetBytes(msg), WebSocketMessageType.Text, true, default);
    }

    private async Task Connect()
    {
        _ws = new ClientWebSocket();
        // ElevenLabsOrigin: wss://api.elevenlabs.io, or a residency host (.eu./.in./.sg.)
        var url = $"{_settings.ElevenLabsOrigin}/v1/convai/conversation?agent_id={_settings.ElevenLabsAgentId}";
        await _ws.ConnectAsync(new Uri(url), default);
        await _ws.SendAsync(Encoding.UTF8.GetBytes(
            JsonSerializer.Serialize(new { type = "conversation_initiation_client_data" })),
            WebSocketMessageType.Text, true, default);
        _ = Task.Run(ReceiveLoop);
    }

    private async Task ReceiveLoop()
    {
        var buf = new byte[32768]; var sb = new StringBuilder();
        while (_ws.State == WebSocketState.Open)
        {
            sb.Clear(); WebSocketReceiveResult r;
            do { r = await _ws.ReceiveAsync(buf, default); sb.Append(Encoding.UTF8.GetString(buf, 0, r.Count)); }
            while (!r.EndOfMessage);

            using var doc = JsonDocument.Parse(sb.ToString());
            var type = doc.RootElement.GetProperty("type").GetString();
            if (type == "audio")            // ElevenLabs audio -> call
                Emit(Convert.FromBase64String(doc.RootElement
                    .GetProperty("audio_event").GetProperty("audio_base_64").GetString()));
            else if (type == "ping")
                await _ws.SendAsync(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(new {
                    type = "pong", event_id = doc.RootElement.GetProperty("ping_event").GetProperty("event_id").GetInt32() })),
                    WebSocketMessageType.Text, true, default);
            else if (type == "interruption")  // barge-in: drop any agent audio still queued
                FlushMedia?.Invoke(this, EventArgs.Empty);
        }
    }

    // slice PCM into 20 ms / 640-byte frames the media platform expects
    private void Emit(byte[] pcm)
    {
        var all = new List<AudioMediaBuffer>(); long tick = DateTime.Now.Ticks;
        for (int off = 0; off < pcm.Length; off += 640)
        {
            var frame = new byte[640];
            Array.Copy(pcm, off, frame, 0, Math.Min(640, pcm.Length - off));
            all.AddRange(Utilities.CreateAudioMediaBuffers(frame, tick, _logger));
            tick += 20 * 10000;
        }
        if (all.Count > 0) SendMediaBuffer?.Invoke(this, new MediaStreamEventArgs { AudioMediaBuffers = all });
    }
}
```

Les deux côtés utilisent du PCM mono 16 kHz, il s’agit donc d’un relais base64. Configurez l’agent sur `pcm_16000`. Lors d’une `interruption` ElevenLabs (interruption de parole), le pont déclenche `FlushMedia`. Reliez-le à votre flux média pour supprimer tous les `AudioMediaBuffer` en file d’attente, sans quoi l’agent continue à parler par-dessus l’appelant. La référence complète des messages se trouve dans la [documentation WebSocket](/docs/fr/eleven-agents/libraries/web-sockets). La fin d’appel et le transfert assisté sont abordés dans les sections ci-dessous.

> **Note**
>
> L’URL dans `Connect()` atteint un agent **public**. Pour un agent privé, demandez côté serveur une URL signée
> de courte durée, `GET /v1/convai/conversation/get-signed-url?agent_id=...` avec votre clé API, puis connectez-vous à l’URL renvoyée. Pour la [résidence des données](/docs/fr/overview/administration/data-residency), définissez `ElevenLabsOrigin` sur votre hôte de résidence des données
> (`wss://api.eu.residency.elevenlabs.io`, `.in.` ou `.sg.`), les requêtes d’URL signée utilisent l’hôte
> `https://` correspondant.

## Étape 4 : le rendre appelable dans Teams

1. **Activez Calling** sur le canal Teams de l’Azure Bot et définissez le **webhook d’appel** sur `https://YOUR_FQDN/api/calling` :

   ```bash
   az bot msteams create -g $RG -n el-teams-agent-bot \
     --enable-calling --calling-web-hook "https://YOUR_FQDN/api/calling"
   ```

   Dans le portail, vous le trouverez dans votre ressource Azure Bot → **Channels** → **Microsoft Teams** → onglet **Calling** :

   ![Le volet Azure Bot Channels indiquant que le canal Microsoft Teams est
   opérationnel](/docs/_fern-img/01115d26ab645dd6ec6569a36a918cd4de0214c79dcb0070d7a3c3a5e544c93b.webp)

   ![L’onglet Calling du canal Teams, avec Enable calling activé et le webhook d’appel
   défini](/docs/_fern-img/7c4819f64878bfb152661adc948c1170f9efd48ba3d68401c4a4d4ba075951b0.webp)

2. Créez un **manifeste d’application Teams** avec `bots[0].supportsCalling: true` et l’ID d’application du bot, puis chargez-le de façon indépendante (**Apps → Manage your apps → Upload a custom app**) ou publiez-le dans toute l’organisation sans l’interface : `New-TeamsApp -DistributionMethod organization -Path ./bot-app.zip` (module PowerShell MicrosoftTeams).

Recherchez l’application par son nom dans Teams et appelez-la, le bot répond et l’agent ElevenLabs parle.

![Un appel Teams actif avec le bot de l’agent ElevenLabs
](/docs/_fern-img/0f1fad08a091bcd20ffbbf5c3949b5503f423d8eb6298f464a870b3a0a254726.webp)

> **Note**
>
> Aucun numéro de téléphone ni compte de ressource n’est nécessaire pour les appels en tête-à-tête par nom, ils sont uniquement requis pour les
> appels entrants RTPC. `Calls.AccessMedia.All` permet le pont audio brut.

## Chat textuel (même bot)

Le même Azure Bot peut également répondre aux **messages textuels** dans Teams, les utilisateurs peuvent donc appeler l’agent ou échanger avec lui par chat. Les appels et la messagerie sont des canaux indépendants sur le bot : le webhook d’appel gère la voix et un **point de terminaison de messagerie** Bot Framework (`/api/messages`) gère le chat.

![Une conversation Teams avec le bot de l’agent ElevenLabs répondant à des messages
textuels](/docs/_fern-img/fb75ae17848916790d29c1c0a703051946902893e46ed62884742db36be453b2.webp)

Dirigez le point de terminaison de messagerie du bot vers l’hôte qui le sert (le bot média ou tout autre service, il ne doit pas nécessairement s’agir de la VM Windows) :

```bash
az bot update -g $RG -n el-teams-agent-bot --endpoint "https://YOUR_FQDN/api/messages"
```

Implémentez le point de terminaison avec le SDK Bot Framework et transmettez chaque message à l’agent en **mode texte** via le même WebSocket de conversation utilisé pour la voix, envoyez un événement [`user_message`](/docs/fr/eleven-agents/customization/events/client-to-server-events), puis lisez l’événement [`agent_response`](/docs/fr/eleven-agents/customization/events/client-events). Activez d’abord le champ **first message** dans les paramètres d’[overrides](/docs/fr/eleven-agents/customization/personalization/overrides) de l’agent, le code ci-dessous le remplace par une valeur vide afin que la réponse traite le message de l’utilisateur plutôt que le message d’accueil de l’agent :

**`ChatBot.cs : chat textuel Teams -> ElevenLabs (mode texte)`**

```csharp title="ChatBot.cs : chat textuel Teams -> ElevenLabs (mode texte)"
public class ChatBot : ActivityHandler
{
    private readonly AppSettings _settings;
    public ChatBot(AppSettings settings) => _settings = settings;

    protected override async Task OnMessageActivityAsync(
        ITurnContext<IMessageActivity> turn, CancellationToken ct)
    {
        var reply = await AskAgent(turn.Activity.Text, ct);
        await turn.SendActivityAsync(MessageFactory.Text(reply), ct);
    }

    private async Task<string> AskAgent(string text, CancellationToken ct)
    {
        using var ws = new ClientWebSocket();
        var url = $"{_settings.ElevenLabsOrigin}/v1/convai/conversation?agent_id={_settings.ElevenLabsAgentId}";
        await ws.ConnectAsync(new Uri(url), ct);
        // Suppress the agent's greeting: with no override, the first agent_response is the
        // configured first message, not the answer to this user_message.
        await Send(ws, new
        {
            type = "conversation_initiation_client_data",
            conversation_config_override = new { agent = new { first_message = "" } },
        }, ct);
        await Send(ws, new { type = "user_message", text }, ct);

        var buf = new byte[16384]; var sb = new StringBuilder();
        while (ws.State == WebSocketState.Open)
        {
            sb.Clear(); WebSocketReceiveResult r;
            do { r = await ws.ReceiveAsync(buf, ct); sb.Append(Encoding.UTF8.GetString(buf, 0, r.Count)); }
            while (!r.EndOfMessage);

            using var doc = JsonDocument.Parse(sb.ToString());
            switch (doc.RootElement.GetProperty("type").GetString())
            {
                case "agent_response":
                    return doc.RootElement.GetProperty("agent_response_event")
                              .GetProperty("agent_response").GetString();
                case "ping":
                    await Send(ws, new { type = "pong", event_id = doc.RootElement
                        .GetProperty("ping_event").GetProperty("event_id").GetInt32() }, ct);
                    break;
            }
        }
        return "Sorry, I couldn't reach the agent.";
    }

    private static Task Send(ClientWebSocket ws, object msg, CancellationToken ct) =>
        ws.SendAsync(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(msg)),
            WebSocketMessageType.Text, true, ct);
}
```

Enregistrez-le de la manière habituelle (un `CloudAdapter`, le bot via `AddTransient<IBot, ChatBot>()` et un contrôleur `/api/messages`), puis ajoutez des étendues de chat à l’entrée du bot dans le manifeste :

```json
"bots": [
  { "botId": "YOUR_APP_ID", "supportsCalling": true, "scopes": ["personal", "team", "groupChat"] }
]
```

> **Note**
>
> L’extrait ouvre une nouvelle conversation par message, chaque tour est donc indépendant. Pour conserver la
> **mémoire** du chat, maintenez un WebSocket ouvert par `conversation.id` Teams (réutilisez-le entre les tours) et fermez les
> sessions inactives, l’agent se souviendra alors des messages précédents de cette conversation. Le remplacement `first_message`
> doit être activé dans les paramètres d’[overrides](/docs/fr/eleven-agents/customization/personalization/overrides)
> de l’agent, le serveur **ferme la conversation** si un remplacement non autorisé est envoyé. Si vous ne pouvez pas l’activer, omettez le
> remplacement et ignorez plutôt le premier `agent_response` de chaque session (le message d’accueil), puis renvoyez
> le suivant.

> **Tip**
>
> Si les réponses du chat n’arrivent jamais, activez l’**événement client `agent_response`** [client event](/docs/fr/eleven-agents/customization/events/client-events) dans les paramètres **Advanced**
> de l’agent, les réponses textuelles sont transmises par cet événement.

## Fin d’appel

Quand ElevenLabs termine la conversation (son outil **End Call** ferme le WebSocket), raccrochez la partie Teams :

```csharp
await this.Call.DeleteAsync(); // after a short delay so the goodbye audio finishes
```

## Transfert assisté vers un humain

L’agent déclenche un outil client personnalisé `transfer_to_human`. Le bot **invite un utilisateur Teams** dans l’appel en cours (ajout consultatif), puis se retire :

```csharp
var target = new IdentitySet { User = new Identity { Id = humanObjectId } };
await this.Call.Participants.InviteAsync(target, replacesCallId: null);
// suppress the end-call hangup while transferring, and mute the bot
```

> **Tip**
>
> Le transfert consultatif (`replacesCallId`) exige que les deux parties soient des utilisateurs Teams du \*\*même
> tenant \*\*. Les cibles de transfert RTPC nécessitent une instance d’application. Pour informer d’abord l’humain, transmettez un paramètre
> `reason` depuis l’agent et diffusez-le à l’humain avant la mise en relation.

## Dépannage

#### \`MediaPlatform needs a system with at least 2 cores\`

La VM ne possède qu’un seul cœur physique. Redimensionnez-la avec ≥ 2 cœurs physiques (par exemple, `D4s_v3`) et redémarrez-la.

#### \`Unable to load DLL 'NativeMedia'\`

Installez le **VC++ Redistributable** (`vcredist140`) et la fonctionnalité Windows
**Server-Media-Foundation**, puis redémarrez le bot.

#### L’appel entrant renvoie 500 / l’appel ne se connecte pas

Il s’agit du bug de port nul d’EchoBot sur 443, corrigez `HttpHelpers.SetAbsoluteUri` (voir l’étape 3). Vérifiez également
que le certificat est signé par une AC et accessible sur le port 443.

#### L’appel au bot indique « we couldn't connect you »

Vérifiez que Calling est activé sur le canal Teams avec le webhook `/api/calling` correct, que l’autorisation
Graph `Calls.AccessMedia.All` a reçu le consentement et que les ports 443/8445/9441 sont ouverts à la fois dans le
NSG et le pare-feu Windows. Si les appels **fonctionnaient auparavant puis se sont arrêtés**, vérifiez que le processus du bot
fonctionne toujours sur la VM, la limite d’exécution par défaut de 72 heures du Planificateur de tâches l’arrête quelques
jours après le démarrage (voir l’avertissement de l’étape 3).

## Liens utiles

* [Présentation des bots d’appels et de réunions](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/calls-and-meetings/calls-meetings-bots-overview)
* [Concepts de médias en temps réel](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/calls-and-meetings/real-time-media-concepts)
* [Enregistrer un bot d’appel](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/calls-and-meetings/registering-calling-bot)
* [microsoft-graph-comms-samples (EchoBot)](https://github.com/microsoftgraph/microsoft-graph-comms-samples)
* [call: transfer (consultatif)](https://learn.microsoft.com/en-us/graph/api/call-transfer)
* [Protocole WebSocket de l’agent](/docs/fr/eleven-agents/libraries/web-sockets)