Créer un chat vidéo avec ASP.NET Core Blazor et Twilio

August 25, 2020
Rédigé par
David Pine
Contributeur
Les opinions exprimées par les contributeurs de Twilio sont les leurs
Révisé par
AJ Saulsberry
Contributeur
Les opinions exprimées par les contributeurs de Twilio sont les leurs

Construire une application Web de chat vidéo avec ASP.NET Core Blazor et Twilio Programmable Video

L'interaction utilisateur en temps réel est un excellent moyen d'améliorer les capacités de communication et de collaboration d'une application web. Le chat vidéo avec des collègues, des amis ou des membres de la famille est devenu la nouvelle norme, et est un choix évident pour les sites de vente, de support client et de formation. Pour les travailleurs à distance, le chat vidéo améliore l'efficacité de la collaboration d'équipe.

Cependant, est-ce que l'implémentation du chat vidéo est pratique ?

Si vous développez avec Blazor WebAssembly (WASM) sur le front-end et ASP.NET Core pour votre serveur, la réponse est : oui. Twilio Programmable Video et les helper librairies Twilio pour JavaScript et .NET vous permettent d'ajouter efficacement un chat vidéo robuste à votre application.

Que vous construisiez des solutions dédiées à la télémédecine, à l'enseignement à distance ou à l'engagement des employés, Twilio est conforme au RGPD et à l'HIPAA. Grâce à Twilio Programmable Video, vous pouvez créer des applications vidéo sécurisées et évolutives.

Dans ce post, vous apprendrez à créer une application de chat vidéo entièrement fonctionnelle à l'aide du SDK JavaScript Twilio dans votre application monopage (Single Page Application) Blazor, du SDK Twilio pour C# et de .NET dans votre code serveur ASP.NET Core. Vous allez construire les interactions nécessaires pour créer et rejoindre des salons de chat vidéo, et pour publier et s'abonner à des pistes audio et vidéo des participants.

Conditions préalables

Pour construire le projet décrit dans ce post, vous aurez besoin des technologies et des outils suivants :

Logiciels et services

  • SDK .NET Core 3.1 : version 3.1.300 ou ultérieure.
  • Node.js et npm : le programme d'installation Node.js installe également npm.
  • Visual Studio CodeVisual Studio 2019 : version 16.7.2 ou supérieure, ou un autre IDE compatible avec les versions ci-dessus.
  • Git : nécessaire si vous voulez cloner le répertoire ou utiliser Git pour la gestion du code source.
  • Compte Font Awesome : copiez votre balise <script> dans un endroit sûr.
  • Compte Twilio : inscrivez-vous avec ce lien pour recevoir un crédit supplémentaire de 10 $.

Matériel

Pour tester pleinement l'application terminée, vous aurez besoin du matériel suivant :

  • Un périphérique vidéo connecté, tel que la webcam intégrée à un ordinateur portable
  • Un deuxième périphérique vidéo, comme le très réputé Microsoft LifeCam Studio

Connaissances et expérience

Pour tirer le meilleur parti de ce post, vous devez avoir :

Un répertoire pour ce post est disponible sur GitHub. Il contient le code source complet de ce tutoriel.

Mise en route avec Twilio Programmable Video

Vous aurez besoin d'un compte d'essai Twilio gratuit et d'un projet Twilio Programmable Video pour pouvoir construire ce projet avec le SDK Twilio Video. La configuration ne prendra que quelques minutes.

Une fois que vous avez un compte Twilio, accédez à la console Twilio et effectuez les opérations suivantes :

  1. Dans l'accueil du tableau de bord, localisez votre Account SID et votre Auth Token (token d'autorisation), et copiez-les dans un endroit sûr.
  2. Sélectionnez la section Programmable Video de la console.
  3. Dans Tools (Outils) > API Keys (Clés API), créez une nouvelle clé API avec le nom de votre choix et copiez le SID de compte et la clé secrète API dans un endroit sûr.

Les informations d'identification que vous venez d'acquérir sont des secrets d'utilisateur. Il est donc conseillé de ne pas les stocker dans le code source du projet. Une façon de les protéger et de les rendre accessibles dans la configuration de votre projet consiste à les stocker en tant que variables d'environnement sur votre machine de développement.

ASP.NET Core peut accéder aux variables d'environnement par le biais du package Microsoft.Extensions.Configuration afin qu'elles puissent être utilisées comme propriétés d'un objet IConfiguration dans la classe Startup. Les instructions suivantes vous indiquent comment procéder sous Windows.

Exécutez les commandes suivantes dans l'invite de commande Windows ou PowerShell, en remplaçant les espaces réservés par vos informations d'identification. Pour les autres systèmes d'exploitation, utilisez des commandes comparables pour créer les mêmes variables d'environnement.

setx TWILIO_ACCOUNT_SID [Account SID]
setx TWILIO_API_SECRET [API Secret]
setx TWILIO_API_KEY [SID]

Si vous préférez, ou si votre environnement de développement l'exige, vous pouvez placer ces valeurs dans le fichier appsettings.development.json comme suit, mais veillez à ne pas exposer ce fichier dans un référentiel de code source ou un autre emplacement facilement accessible.

{
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "AllowedHosts": "*",
  "TwilioAccountSid":"AccountSID",
  "TwilioApiSecret":"API Secret",
  "TwilioApiKey":"SID"
}

Vous pouvez ajouter le fichier appsettings.development.json à votre .gitignore pour que cette solution protège vos informations d'identification.

Création de la solution de chat vidéo

Vous pouvez utiliser l'outillage .NET pour créer des applications Web Blazor WebAssembly (WASM) à partir de l'interface utilisateur de Visual Studio 2019 ou de la CLI .NET Core. Lorsque vous exécutez l'un ou l'autre, l'outillage crée un fichier Visual Studio Solution (.sln) et trois fichiers de projet C# (.csproj) :

Un projet ASP.NET Core 3.1 :

Blazor.Twilio.Video.Server : responsable du service de l'application client Blazor WASM auprès des navigateurs clients et de la fourniture d'une API Web.

Deux projets .NET Standard 2.1 :

Blazor.Twilio.Video.Client : responsable de l'interface utilisateur, notamment comment créer et rejoindre des salles de chat vidéo, et de l'hébergement du flux vidéo des participants.

Blazor.Twilio.Video.Shared : utilisé par les projets .Client et .Server pour combler le fossé entre l'API Web du serveur et les appels HTTP du client avec des graphiques d'objets communs.

Voir les frameworks cibles dans les projets de type SDK pour plus d'informations sur le fonctionnement des frameworks cibles et les combinaisons valides de structures.

Visual Studio 2019 : dans la barre de menu, sélectionnez File (Fichier) > New Project (Nouveau projet). Sélectionnez Blazor App dans la liste des modèles.

La fenêtre Configure your new project (Configurer votre nouveau projet) doit s'ouvrir et afficher « Blazor App » comme type de projet. Dans Project name (Nom du projet), entrez « Blazor.Twilio.Video ». Veillez à inclure les points. Choisissez un répertoire local approprié pour Location (Emplacement).

Cochez Place solution and project in the same directory (Placer la solution et le projet dans le même répertoire). Comme l’éditeur crée une structure de dossiers à plusieurs niveaux et plusieurs projets, il place le fichier de solution (.sln) dans un dossier parent et les dossiers de projet en dessous, de manière standard. Placer le fichier de solution dans un dossier séparé crée un niveau supplémentaire, inutile.

CLI .NET Core : exécutez l'instruction de ligne de commande suivante dans le répertoire où vous souhaitez créer le répertoire de premier niveau pour la solution :

dotnet new blazorwasm --hosted -n Blazor.Twilio.Video

Vous devriez voir un certain nombre de lignes de sortie suivies d'un dernier message « Restore succeeded » (Restauration réussie).

Ajout de packages NuGet pour Twilio et SignalR

L'application serveur ASP.NET Core utilisera le SDK pour C# et .NET de Twilio pour accéder à Twilio Programmable Video.

Vous pouvez installer les packages Twilio, SignalR Client et MessagePack NuGet avec Visual Studio 2019 NuGet Package Manager, Package Manager Console ou la CLI .NET Core.

Voici l'instruction de ligne de commande .NET Core CLI dotnet add :

dotnet add Server/Blazor.Twilio.Video.Server.csproj package Twilio

Le fichier Server/Blazor.Twilio.Video.Server.csproj doit inclure les références de package dans un nœud <ItemGroup>, comme illustré ci-dessous, si la commande s'est terminée avec succès. (Les numéros de version de votre projet peuvent être plus élevés.)

<ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="3.2.0" />
    <PackageReference Include="Twilio" Version="5.45.1" />
</ItemGroup>

L'application client Blazor.Twilio.Video utilise le package de protocole SignalR MessagePack pour fournir une conversion en série binaire rapide et compacte. Elle utilisera également le package SignalR Client pour fournir un accès aux hubs SignalR.

Voici l'instruction de ligne de commande dotnet add pour le package client SignalR MessagePack :

dotnet add Client/Blazor.Twilio.Video.Client.csproj package Microsoft.AspNetCore.SignalR.Client

Pour ajouter le package de protocole SignalR MessagePack, utilisez la commande dotnet add suivante :

dotnet add Client/Blazor.Twilio.Video.Client.csproj package Microsoft.AspNetCore.SignalR.Protocols.MessagePack

Le fichier Blazor.Twilio.Video.Client.csproj doit contenir une section <ItemGroup> qui ressemble à ce qui suit :

 <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="3.2.1" />
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Build" Version="3.2.1" PrivateAssets="all" />
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="3.2.1" PrivateAssets="all" />
    <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="3.1.6" />
    <PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="3.1.6" />
    <PackageReference Include="System.Net.Http.Json" Version="3.2.0" />
  </ItemGroup>

Les numéros de version mineurs de votre projet peuvent être plus élevés.

Activation des types de référence nullable

C# 8.0 a introduit les types de référence nullable qui apportent une contribution utile à cette solution. Vous pouvez activer les types de référence nullable en modifiant le fichier projet C# (.csproj).

Dans tous les fichiers de projet .csporj pour les projets .Client.Server et .Shared, ajoutez l'élément XML suivant au nœud <ItemGroup> supérieur :

<Nullable>enable</Nullable>

Suppression des fichiers de modèle inutiles

Vous n'aurez pas besoin de certains fichiers créés. Supprimez les fichiers suivants, mais pas les dossiers :

/Client
    /Pages
        Counter.razor
        FetchData.razor
    /Shared
        NavMenu.razor
        SurveyPrompt.razor
    /wwwroot
        bootstrap/**
        open-iconic/**
/Server
   /Controllers
       WeatherForecastController.cs
/Shared
   WeatherForecast.cs

Lorsque vous avez terminé, la structure des dossiers et des fichiers de l'explorateur doit ressembler à ce qui suit :

Capture d&#x27;écran Code Visual Studio 1 : après la suppression

Création de la structure de dossiers et de fichiers

Ajoutez les dossiers et fichiers suivants aux dossiers et fichiers existants :

/Client
    /Components
        Cameras.razor
        Cameras.razor.cs
    /Interop
        VideoJS.cs
    /Pages
        Index.razor.cs
    /wwwroot
        site.js
/Server
    /Controllers
        TwilioController.cs
    /Hubs
        NotificationHub.cs
    /Options
        TwilioSettings.cs
    /Services
        TwilioService.cs
/Shared
    CameraState.cs
    Device.cs
    HubEndpoint.cs
    RoomDetails.cs
    TwilioJwt.cs

Le dossier Client doit ressembler à ce qui suit :

Capture d&#x27;écran Code Visual Studio 2 : projet client après ajout

Le dossier Server doit ressembler à ce qui suit :

Capture d&#x27;écran Code Visual Studio 2 : projet de serveur après ajout

Le dossier Shared doit ressembler à ce qui suit :

Capture d&#x27;écran Code Visual Studio 2 : projet partagé après ajout

Construction et test de la solution configurée

Construisez et exécutez l'application pour vous assurer qu'elle compile et fonctionne correctement : appuyez sur F5 pour effectuer cette opération à partir de Visual Studio ou de Visual Studio Code, ou exécutez l'application à partir de la CLI .NET avec la commande dotnet run :

dotnet run -p Server/Blazor.Twilio.Video.Server.csproj

Vous devriez voir une page d'accueil par défaut similaire à celle créée par le tooling, mais il manque les pièces fournies par les fichiers que vous avez supprimés.

Fermez le navigateur et terminez la session du terminal.

Création de services

Le code côté serveur doit effectuer plusieurs actions clés, l'une d'entre elles étant de fournir un token Web JSON (JWT) au client pour que le client puisse se connecter à l'API Twilio Programmable Video. Pour ce faire, vous devez disposer de votre Account SID Twilio, de la clé API et du secret d'API que vous avez stockés en tant que variables d'environnement. Dans ASP.NET Core, il est courant d'utiliser une classe C# fortement typée pour représenter les différents paramètres.

Remplacez le contenu du fichier Server/Options/TwilioSettings.cs par le code C# suivant :

namespace Blazor.Twilio.Video.Server.Options
{
    public class TwilioSettings
    {
        /// <summary>
     /// The primary Twilio account SID, displayed prominently on your twilio.com/console dashboard.
     /// </summary>
        public string? AccountSid { get; set; }

        /// <summary>
        /// Signing Key SID, also known as the API SID or API Key.
        /// </summary>
        public string? ApiKey { get; set; }

        /// <summary>
        /// The API Secret that corresponds to the <see cref="ApiKey"/>.
        /// </summary>
        public string? ApiSecret { get; set; }
    }
}

Ces paramètres sont configurés dans la méthode Startup.ConfigureServices, qui mappe les valeurs des variables d'environnement et du fichier appsettings.json aux instances IOptions<TwilioSettings> disponibles pour l'injection de dépendance. Dans ce cas, les variables d'environnement sont les seules valeurs requises pour la classe TwilioSettings.

Insérez le code C# suivant dans le fichier Shared/RoomDetails.cs :

namespace Blazor.Twilio.Video.Shared
{
    public class RoomDetails
    {
        public string? Id { get; set; } = null!;
        public string? Name { get; set; } = null!;
        public int ParticipantCount { get; set; }
        public int MaxParticipants { get; set; }
    }
}

La classe RoomDetails est un objet qui représente une salle de chat vidéo. Vous aurez également besoin d'un objet simple pour représenter le token Web JSON Twilio (JWT). Ajoutez le code C# ci-dessous au fichier Shared/TwilioJwt.cs :

namespace Blazor.Twilio.Video.Shared
{
    public class TwilioJwt
    {
        public string? Token { get; set; } = null!;
    }
}

Notez l'utilisation de ! (null-forgiving) operator C# 8.0 dans la déclaration de propriété.

Pour implémenter VideoService, remplacez le contenu du fichier Server/Services/TwilioService.cs par le code suivant :

using Blazor.Twilio.Video.Server.Options;
using Blazor.Twilio.Video.Shared;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Twilio;
using Twilio.Base;
using Twilio.Jwt.AccessToken;
using Twilio.Rest.Video.V1;
using Twilio.Rest.Video.V1.Room;
using MicrosoftOptions = Microsoft.Extensions.Options;
using ParticipantStatus = Twilio.Rest.Video.V1.Room.ParticipantResource.StatusEnum;

namespace Blazor.Twilio.Video.Server.Services
{
    public class TwilioService
    {
        readonly TwilioSettings _twilioSettings;

        public TwilioService(MicrosoftOptions.IOptions<TwilioSettings> twilioOptions)
        {
            _twilioSettings =
                twilioOptions?.Value
             ?? throw new ArgumentNullException(nameof(twilioOptions));

            TwilioClient.Init(_twilioSettings.ApiKey, _twilioSettings.ApiSecret);
        }

        public TwilioJwt GetTwilioJwt(string? identity) =>
            new TwilioJwt
            {
                Token = new Token(
                    _twilioSettings.AccountSid,
                    _twilioSettings.ApiKey,
                    _twilioSettings.ApiSecret,
                    identity ?? Guid.NewGuid().ToString(),
                    grants: new HashSet<IGrant> { new VideoGrant() })
                .ToJwt()
            };

        public async ValueTask<IEnumerable<RoomDetails>> GetAllRoomsAsync()
        {
            var rooms = await RoomResource.ReadAsync();
            var tasks = rooms.Select(
                room => GetRoomDetailsAsync(
                    room,
                    ParticipantResource.ReadAsync(
                        room.Sid,
                        ParticipantStatus.Connected)));

            return await Task.WhenAll(tasks);

            static async Task<RoomDetails> GetRoomDetailsAsync(
                RoomResource room,
                Task<ResourceSet<ParticipantResource>> participantTask)
            {
                var participants = await participantTask;
                return new RoomDetails
                {
                    Name = room.UniqueName,
                    MaxParticipants = room.MaxParticipants ?? 0,
                    ParticipantCount = participants.Count()
                };
            }
        }
    }
}

Le constructeur de classe TwilioService prend une instance IOptions<TwilioSettings> et initialise le TwilioClient, en fonction de la clé API fournie et du secret d'API correspondant. Cette opération est effectuée de manière statique et permet une utilisation future de diverses fonctions basées sur les ressources. L'implémentation de GetTwilioJwt est utilisée pour émettre un nouveau Twilio.JWT.AccessToken.Token, en fonction de l'Account SID, de la clé API, du secret d'API, de l'identité et d'une nouvelle instance de HashSet<iGrant> avec un seul objet VideoGrant. Avant le renvoi, une invocation de la fonction .ToJwt convertit l'instance de token en son équivalent string.

La fonction GetAllRoomsAsync renvoie une liste d'objets RoomDetails. Il commence par attendre la fonction RoomResource.ReadAsync, qui donnera un ResourceSet<RoomResource> une fois l'opération asynchrone retournée. À partir de cette liste de salles, le code projette une série de Task<RoomDetails> où il demandera le ResourceSet<ParticipantResource> correspondant actuellement connecté à la salle spécifiée avec l'identifiant de salle room.UniqueName.

Vous remarquerez peut-être une syntaxe inconnue dans la fonction GetAllRoomsService si vous n'avez pas l'habitude de coder après l'instruction return. C# 8 inclut une fonction locale statique qui permet d'écrire des fonctions dans la portée du corps de la méthode (« localement »), même après l'instruction return. Elles sont statiques pour s'assurer que les variables ne sont pas capturées dans la portée.

Notez que pour chaque salle n qui existe, GetRoomDetailsAsync est appelé pour extraire les participants connectés à la salle. Cela peut être un problème de performance ! Même si cette opération est effectuée de manière asynchrone et parallèle, elle doit être considérée comme un goulet d'étranglement potentiel et marquée pour la refactorisation. Ce n'est pas un problème dans ce projet de démonstration, car il y a quelques salles tout au plus.

Création du contrôleur API

Le contrôleur vidéo fournira deux points de terminaison HTTP GET que le client Blazor pourra utiliser.

Point de terminaison

Verbe

Type

Description

api/twilio/token

GET

JSON

un objet avec un membre token attribué à partir du TWT Twilio

api/twilio/rooms

GET

JSON

tableau des détails de la salle : 
{ name, participantCount, maxParticipants }

Remplacez le contenu du fichier Server/Controllers/TwilioController.cs par le code C# suivant :

using Blazor.Twilio.Video.Server.Services;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;

namespace Blazor.Twilio.Video.Server.Controllers
{
    [
        ApiController,
        Route("api/twilio")
    ]
    public class TwilioController : ControllerBase
    {
        [HttpGet("token")]
        public IActionResult GetToken(
            [FromServices] TwilioService twilioService) =>
             new JsonResult(twilioService.GetTwilioJwt(User.Identity.Name));

        [HttpGet("rooms")]
        public async Task<IActionResult> GetRooms(
            [FromServices] TwilioService twilioService) =>
            new JsonResult(await twilioService.GetAllRoomsAsync());
    }
}

Le contrôleur est décoré avec l'attribut ApiController et un attribut Route contenant le modèle "api/twilio".

Dans les actions TwilioControllerVideoService est injecté à l'aide de FromServicesAttribute, qui fournit l'instance aux méthodes.

Création du hub de notification

L'application ASP.NET Core ne serait pas complète sans l'utilisation de SignalR, qui « ...est une bibliothèque open-source qui simplifie l'ajout de fonctionnalités Web en temps réel aux applications. La fonctionnalité Web en temps réel permet au code côté serveur de transmettre instantanément du contenu aux clients. »

Lorsqu'un utilisateur crée une salle dans l'application, son code côté client informe le serveur et, au final les autres clients, de la nouvelle salle. Pour ce faire, vous avez accès à un hub de notification SignalR.

Ajoutez le code C# suivant dans le fichier Shared/HubEndpoint.cs :

namespace Blazor.Twilio.Video.Shared
{
    public class HubEndpoints
    {
        public const string NotificationHub = "/notifications";
        public const string RoomsUpdated = nameof(RoomsUpdated);
    }
}

Remplacez le contenu du fichier Server/Hubs/NotificationHub.cs par le code C# suivant :

using Blazor.Twilio.Video.Shared;
using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;

namespace Blazor.Twilio.Video.Server.Hubs
{
    public class NotificationHub : Hub
    {
        public Task RoomsUpdated(string room) =>
            Clients.All.SendAsync(HubEndpoints.RoomsUpdated, room);
    }
}

Le NotificationHub envoie de manière asynchrone un message à tous les autres clients pour les avertir lorsqu'une salle est ajoutée.

Configuration de la classe Startup du projet Server

Quelques éléments doivent être mis à jour dans la classe Startup et dans la méthode ConfigureServices.

Remplacez les déclarations C# using en haut du fichier Server/Startup.cs :

using Blazor.Twilio.Video.Server.Hubs;
using Blazor.Twilio.Video.Server.Options;
using Blazor.Twilio.Video.Server.Services;
using Blazor.Twilio.Video.Shared;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.ResponseCompression;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Net.Http.Headers;
using System.Linq;
using static System.Environment;

Dans la méthode ConfigureServices, remplacez tout le code existant par le suivant :

services.AddSignalR(options => options.EnableDetailedErrors = true)
        .AddMessagePackProtocol();
services.Configure<TwilioSettings>(settings =>
{
    settings.AccountSid = GetEnvironmentVariable("TWILIO_ACCOUNT_SID");
    settings.ApiSecret = GetEnvironmentVariable("TWILIO_API_SECRET");
    settings.ApiKey = GetEnvironmentVariable("TWILIO_API_KEY");
});
services.AddSingleton<TwilioService>();
services.AddControllersWithViews();
services.AddRazorPages();
services.AddResponseCompression(opts =>
    opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
        new[] { "application/octet-stream" }));

Cela configure les paramètres de l'application contenant les informations d'identification de l'API Twilio, mappe le service vidéo à son implémentation correspondante, attribue le chemin racine à la SPA et ajoute SignalR.

Remplacez le corps de la méthode Configure par le code C# suivant :

app.UseResponseCompression();

if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
    app.UseWebAssemblyDebugging();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseBlazorFrameworkFiles();
app.UseStaticFiles(new StaticFileOptions
{
    HttpsCompression = HttpsCompressionMode.Compress,
    OnPrepareResponse = context =>
        context.Context.Response.Headers[HeaderNames.CacheControl] =
            $"public,max-age={86_400}"
});
app.UseRouting();
app.UseEndpoints(endpoints =>
{
    endpoints.MapRazorPages();
    endpoints.MapControllers();
    endpoints.MapHub<NotificationHub>(HubEndpoints.NotificationHub);
    endpoints.MapFallbackToFile("index.html");
});

Cette opération mappe le point de terminaison de notification sur l'implémentation de NotificationHub. À l'aide de ce point de terminaison, la SPA Angular exécutée dans les navigateurs clients peut envoyer des messages à tous les autres clients. SignalR fournit l'infrastructure de notification pour ce processus.

La configuration côté serveur est terminée. À partir de la ligne de commande ou de la fenêtre de terminal, exécutez la commande dotnet build pour vous assurer que l'application se compile. Il ne devrait y avoir aucune erreur à ce stade, mais vous devriez vous attendre à un ou plusieurs avertissements ; vous allez les résoudre bientôt.

Finalisation des classes de bibliothèque Shared

Le projet Shared est destiné à contenir des objets communs utilisés à la fois par Server et Client. Puisque vous utilisez Blazor WebAssembly sur le client, vous pouvez utiliser des objets C# comme vous le faites sur le serveur.

Ajoutez le code C# suivant dans le fichier Shared/CameraState.cs :

namespace Blazor.Twilio.Video.Shared
{
    public enum CameraState
    {
        LoadingCameras,
        FoundCameras,
        Error
    }
}

Ajoutez le code C# suivant dans le fichier Shared/Device.cs :

namespace Blazor.Twilio.Video.Shared
{
    public class Device
    {
        public string DeviceId { get; set; } = null!;
        public string Label { get; set; } = null!;
    }
}

Les autres objets doivent déjà avoir été exécutés au cours des étapes précédentes. À ce stade, vous ne devriez pas avoir à modifier quoi que ce soit dans les projets Server ou Shared. Vous avez une bibliothèque partagée et un projet de serveur ASP.NET Core qui sont tous les deux prêts.

Construction du service d'opérateur intermédiaire

La classe VideoJS sert d'opérateur intermédiaire entre le code C# Blazor WebAssembly et le JavaScript exécuté dans le navigateur client. L'une des plus grandes idées reçues sur WebAssembly est que JavaScript n'est plus nécessaire. Ce n'est pas vrai. En fait, ils se complètent.

Ajoutez le code C# suivant dans le fichier Client/Interop/VideoJS.cs :

using Blazor.Twilio.Video.Shared;
using Microsoft.JSInterop;
using System.Threading.Tasks;

namespace Blazor.Twilio.Video.Client.Interop
{
    public static class VideoJS
    {
        public static ValueTask<Device[]> GetVideoDevicesAsync(
              this IJSRuntime? jsRuntime) =>
              jsRuntime?.InvokeAsync<Device[]>(
                  "videoInterop.getVideoDevices") ?? new ValueTask<Device[]>();

        public static ValueTask StartVideoAsync(
            this IJSRuntime? jSRuntime,
            string deviceId,
            string selector) =>
            jSRuntime?.InvokeVoidAsync(
                "videoInterop.startVideo",
                deviceId, selector) ?? new ValueTask();

        public static ValueTask<bool> CreateOrJoinRoomAsync(
            this IJSRuntime? jsRuntime,
            string roomName,
            string token) =>
            jsRuntime?.InvokeAsync<bool>(
                "videoInterop.createOrJoinRoom",
                roomName, token) ?? new ValueTask<bool>(false);

        public static ValueTask LeaveRoomAsync(
            this IJSRuntime? jsRuntime) =>
            jsRuntime?.InvokeVoidAsync(
                "videoInterop.leaveRoom") ?? new ValueTask();
    }
}

Ces fonctions d'opérateur intermédiaire exposent la fonctionnalité JavaScript au code Blazor WebAssembly et renvoient des valeurs de JavaScript à Blazor.

Construction de la SPA Blazor WebAssembly côté client

Si vous vous en souvenez, vous avez supprimé Bootstrap et Open-Iconic plus tôt dans ce tutoriel. Au lieu de faire en sorte que l'exécutable de votre serveur fournisse ces fichiers statiques à l'application client, vous pouvez compter sur des réseaux de diffusion de contenu tiers (CDN), tels que https://cdnjs.com.

Remplacez le contenu de Client/wwwroot/index.html par le balisage suivant :

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
        <title>Blazor.Twilio.Video</title>
        <base href="/" />
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootswatch/4.5.0/darkly/bootstrap.min.css" />
        <link href="css/app.css" rel="stylesheet" />
        <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
        <script src="//media.twiliocdn.com/sdk/js/video/releases/2.3.0/twilio-video.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.5.0/js/bootstrap.bundle.min.js"></script>
    </head>
    <body>
        <app>
            <div class="d-flex justify-content-center align-items-center w-100 p-4">
                <h1 class="display-1 twilio-text">
                    <strong class="pr-4">Loading...</strong>
                    <span class="spinner-grow ml-auto" role="status" aria-hidden="true"></span>
                </h1>
            </div>
        </app>
        <div id="blazor-error-ui">
            An unhandled error has occurred.
            <a href="" class="reload">Reload</a>
            <a class="dismiss">🗙</a>
        </div>
        <script src="_framework/blazor.webassembly.js"></script>
        <script src="site.js"></script>
    </body>
</html>

Vous aurez besoin de votre balise de script Font Awesome pour configurer complètement l'interface utilisateur. Si vous avez besoin de trouver la balise créée lors de votre inscription, rendez-vous sur https://fontawesome.com/kits et cliquez sur le numéro de code sous Your kits (Vos kits). 

Dans le fichier index.html, collez votre balise Font Awesome <script> dans l'élément <head> immédiatement après le nœud <link href="CSS/app.CSS" rel="stylesheet" />.

Le fichier index.html est composé d'un HTML plutôt standard, mais regardez l'élément <app> dans l'élément <body>. C'est là que les choses deviennent intéressantes. Tout comme les autres frameworks d'applications monopage (SPA), Blazor nomme son élément hôte cible app par défaut, bien que cela puisse être modifié. Lorsque Blazor commence à s'exécuter, il s'accroche à cet élément DOM et sert d'ancrage pour l'expérience utilisateur côté client. Tous les balisages à l'intérieur de l'élément <app>…</app> sont remplacés lorsque l'application est entièrement opérationnelle. Vous pouvez placer ici un balisage pour représenter une sorte d'indicateur de chargement, ce qui est utile pour indiquer la progression aux utilisateurs finaux.

Remplacez le contenu du fichier Client/wwwroot/css/app.css par le balisage CSS suivant :

:root {
    --twilio-red: #F22F46;
    --twilio-blue: #0D122B;
}

.twilio-text {
    color: var(--twilio-red);
}

a.list-group-item.list-group-item-action.active {
    background-color: var(--twilio-blue);
    border-color: var(--twilio-blue);
}

.twilio-btn-red {
    background-color: var(--twilio-red);
    border-color: var(--twilio-red);
    color: #fff;
}

    .twilio-btn-red:not(:disabled):hover {
        background-color: #D31027;
        border-color: #D31027;
    }

.twilio-btn-blue {
    background-color: var(--twilio-blue);
    border-color: var(--twilio-blue);
    color: #fff;
}

    .twilio-btn-blue:not(:disabled):hover {
        background-color: #00000C;
        border-color: #00000C;
    }

input:disabled, .btn:disabled {
    border-color: #444;
    cursor: not-allowed;
}

audio {
    display: none;
}

.participants-grid {
    display: grid;
    grid-gap: 5px;
    grid-template-rows: 1fr 1fr;
    grid-template-columns: 1fr 1fr;
}
    .participants-grid > div:nth-of-type(1) {
        grid-row: 1;
        grid-column: 1;
    }
    .participants-grid > div:nth-of-type(2) {
        grid-row: 1;
        grid-column: 2;
    }
    .participants-grid > div:nth-of-type(3) {
        grid-row: 2;
        grid-column: 1;
    }
    .participants-grid > div:nth-of-type(4) {
        grid-row: 2;
        grid-column: 2;
    }

app {
    position: relative;
    display: flex;
    flex-direction: column;
    height: 100vh;
}

.main {
    flex: 1;
}

.content {
    padding-top: 1.1rem;
}

.valid.modified:not([type=checkbox]) {
    outline: 1px solid #26b050;
}

.invalid {
    outline: 1px solid red;
}

.validation-message {
    color: red;
}

La feuille de style en cascade est désormais considérablement simplifiée ; elle ne compte plus que près de la moitié du nombre de lignes. Elle contient deux variables :root qui contiennent les couleurs officielles de Twilio. La classe participant-grid est la façon dont vous utilisez une grille deux par deux, dans laquelle l'une des quatre vignettes représente un participant à la discussion vidéo.

Remplacez le contenu du fichier Client/Shared/MainLayout.razor par le balisage HTML suivant :

@inherits LayoutComponentBase

<div class="container-fluid">
    @Body
</div>

Conformément au principe de séparation des préoccupations, le balisage agit comme un modèle et la logique est séparée dans le fichier Client/Pages/Index.razor.cs. Dans Visual Studio, ces deux fichiers sont réduits l'un sur l'autre, puisqu'on sait par convention qu'ils sont liés.

Le balisage se compose des éléments suivants :

  • le composant de sélection de caméra
  • une entrée permettant de créer une nouvelle salle
  • la liste des salles existantes
  • la grille des participants

Lorsque l'utilisateur saisit un nom de salle, le bouton de création de salle est activé. Si l'utilisateur clique dessus, la logique TryAddRoom est appelée.

Ajoutez le code C# suivant dans le fichier Client/Pages/Index.razor.cs :

using Blazor.Twilio.Video.Client.Interop;
using Blazor.Twilio.Video.Shared;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.JSInterop;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;

namespace Blazor.Twilio.Video.Client.Pages
{
    public partial class Index
    {
        [Inject] 
        protected IJSRuntime? JavaScript { get; set; }
        [Inject] 
        protected NavigationManager NavigationManager { get; set; } = null!;
        [Inject]
        protected HttpClient Http { get; set; } = null!;

        List<RoomDetails> _rooms = new List<RoomDetails>();

        string? _roomName;
        string? _activeCamera;
        string? _activeRoom;
        HubConnection? _hubConnection;

        protected override async Task OnInitializedAsync()
        {
            _rooms = await Http.GetFromJsonAsync<List<RoomDetails>>("api/twilio/rooms");

            _hubConnection = new HubConnectionBuilder()
                .AddMessagePackProtocol()
                .WithUrl(NavigationManager.ToAbsoluteUri(HubEndpoints.NotificationHub))
                .WithAutomaticReconnect()
                .Build();

            _hubConnection.On<string>(HubEndpoints.RoomsUpdated, OnRoomAdded);

            await _hubConnection.StartAsync();
        }

        async ValueTask OnLeaveRoom()
        {
            await JavaScript.LeaveRoomAsync();
            await _hubConnection.InvokeAsync(HubEndpoints.RoomsUpdated, _activeRoom = null);
            if (!string.IsNullOrWhiteSpace(_activeCamera))
            {
                await JavaScript.StartVideoAsync(_activeCamera, "#camera");
            }
        }

        async Task OnCameraChanged(string activeCamera) => 
            await InvokeAsync(() => _activeCamera = activeCamera);

        async Task OnRoomAdded(string roomName) =>
            await InvokeAsync(async () =>
            {
                _rooms = await Http.GetFromJsonAsync<List<RoomDetails>>("api/twilio/rooms");
                StateHasChanged();
            });

        protected async ValueTask TryAddRoom(object args)
        {
            if (_roomName is null || _roomName is { Length: 0 })
            {
                return;
            }

            var takeAction = args switch
            {
                KeyboardEventArgs keyboard when keyboard.Key == "Enter" => true,
                MouseEventArgs _ => true,
                _ => false
            };

            if (takeAction)
            {
                var addedOrJoined = await TryJoinRoom(_roomName);
                if (addedOrJoined)
                {
                    _roomName = null;
                }
            }
        }

        protected async ValueTask<bool> TryJoinRoom(string? roomName)
        {
            if (roomName is null || roomName is { Length: 0 })
            {
                return false;
            }

            var jwt = await Http.GetFromJsonAsync<TwilioJwt>("api/twilio/token");
            if (jwt?.Token is null)
            {
                return false;
            }

            var joined = await JavaScript.CreateOrJoinRoomAsync(roomName, jwt.Token);
            if (joined)
            {
                _activeRoom = roomName;
                await _hubConnection.InvokeAsync(HubEndpoints.RoomsUpdated, _activeRoom);
            }

            return joined;
        }
    }
}

Le code C# Index.razor.cs possède quelques propriétés qui sont décorées avec l'attribut InjectAttribute. Cela indique que leurs implémentations seront résolues à partir de la collection de services d'injection de dépendance et fournies lors de l'exécution :

IJSRuntime représente une instance d'une exécution de JavaScript à laquelle les appels peuvent être envoyés.

NavigationManager fournit une abstraction pour l'interrogation et la gestion de la navigation URI.

HttpClient permet d'envoyer des requêtes HTTP et de recevoir des réponses HTTP à partir d'une ressource identifiée par un URI.

Plusieurs champs contiennent l'état de l'application, tels que les salles existantes, le nom de salle saisi par l'utilisateur, la salle active, la caméra active et une instance de connexion au hub SignalR. OnInitializedAsync est remplacé pour appeler le point de terminaison api/twilio/rooms des serveurs afin d'obtenir les salles actuelles. En outre, il instancie la connexion du hub SignalR à l'aide du protocole MessagePack, avec reconnexion automatique, et enregistre un écouteur sur le point de terminaison « rooms updated » (salles mises à jour), juste avant de démarrer la connexion.

Le balisage Razor correspondant se trouve dans le fichier Client/Pages/Index.razor. Remplacez son contenu par le balisage Razor suivant :

@page "/"
@using Blazor.Twilio.Video.Client.Components

<div class="row h-100 pt-5">
    <div class="col-3">
        <div class="jumbotron p-4">
            <Cameras CameraChanged="OnCameraChanged" />
            <h5><i class="fas fa-video"></i> Rooms</h5>
            <div class="list-group">
                <div class="list-group-item d-flex justify-content-between align-items-center">
                    <div class="input-group">
                        <input type="text" class="form-control form-control-lg"
                               placeholder="Room name" aria-label="Room Name" disabled="@(_activeCamera is null)"
                               @bind="_roomName" @onkeydown="@(async args => await TryAddRoom(args))" />
                        <div class="input-group-append">
                            <button class="btn btn-lg twilio-btn-red"
                                    disabled="@(_activeCamera is null || _roomName is null)"
                                    @onclick="@(async args => await TryAddRoom(args))">
                                <i class="far fa-plus-square" aria-label="Create room"></i> Create
                            </button>
                        </div>
                    </div>
                </div>
                @if (!(_rooms?.Any() ?? false))
                {
                    <div class="list-group-item d-flex justify-content-between align-items-center">
                        <p class="lead mb-0">
                            Add a room to begin. Other online participants can join or create rooms.
                        </p>
                    </div>
                }
                else
                {
                    @foreach (var room in _rooms!)
                    {
                        <a href="#" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center
                           @(room.Name == _activeRoom ? "active" : null)"
                           @onclick="@(async _ => await TryJoinRoom(room.Name))">
                            @room.Name
                            <span class="badge badge-primary badge-pill">
                                @($"{room.ParticipantCount} / {room.MaxParticipants}")
                            </span>
                        </a>
                    }
                }

                @if (_activeRoom != null)
                {
                    <div class="list-group-item d-flex justify-content-between align-items-center">
                        <button class="btn btn-lg twilio-btn-red w-100" @onclick="@(async _ => await OnLeaveRoom())">Leave Room?</button>
                    </div>
                }
            </div>
        </div>
    </div>
    <div class="col-9">
        <div id="participants" class="participants-grid">
            <div class="embed-responsive embed-responsive-16by9">
                <div id="camera" class="embed-responsive-item"></div>
            </div>
        </div>
    </div>
</div>

Le balisage inclut un élément HTML non standard nommé <Cameras />. Cet élément est un composant Razor et contient son propre balisage C# et Razor. Le composant caméra expose un événement nommé CameraChanged. L'événement se déclenche lorsque la sélection de la caméra est effectuée à partir du composant caméra.

Ajoutez le code C# suivant dans le fichier Client/Components/Camera.razor.cs :

using Blazor.Twilio.Video.Client.Interop;
using Blazor.Twilio.Video.Shared;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using System.Threading.Tasks;

namespace Blazor.Twilio.Video.Client.Components
{
    public partial class Cameras
    {
        [Inject]
        protected IJSRuntime? JavaScript { get; set; }

        [Parameter]
        public EventCallback<string> CameraChanged { get; set; }

        protected Device[]? Devices { get; private set; }
        protected CameraState State { get; private set; }
        protected bool HasDevices => State == CameraState.FoundCameras;
        protected bool IsLoading => State == CameraState.LoadingCameras;

        string? _activeCamera;

        protected override async Task OnInitializedAsync()
        {
            Devices = await JavaScript.GetVideoDevicesAsync();
            State = Devices != null && Devices.Length > 0
                    ? CameraState.FoundCameras
                    : CameraState.Error;
        }

        protected async ValueTask SelectCamera(string deviceId)
        {
            await JavaScript.StartVideoAsync(deviceId, "#camera");
            _activeCamera = deviceId;

            if (CameraChanged.HasDelegate)
            {
                await CameraChanged.InvokeAsync(_activeCamera);
            }
        }
    }
}

Blazor utilise le moteur de visualisation Razor pour compiler des vues. Le balisage Razor crée un modèle dans lequel la liaison du modèle de données C# a lieu.

Notez que le composant Cameras est une classe partial. Ceci est nécessaire, car le balisage Razor compile en réalité dans une classe nommée Cameras. Vous devez déclarer votre classe Cameras.razor.cs comme partial.

Ajoutez le balisage Razor suivant dans le fichier Client/Components/Camera.razor :

<h5><i class="fas fa-cog"></i> Settings</h5>
<div class="dropdown pb-4">
    <button class="btn btn-lg btn-secondary dropdown-toggle twilio-btn-red w-100"
            type="button" id="dropdownMenuButton"
            data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
        <span>@(IsLoading ? "Loading cameras..." : "Select Camera")</span>
        @if (IsLoading)
        {
            <span id="loading" class="spinner-border spinner-border-sm"
                  role="status" aria-hidden="true"></span>
        }
    </button>
    <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
        @if (HasDevices)
        {
            foreach (var device in Devices!)
            {
                <a href="#" class="dropdown-item @(_activeCamera == device.DeviceId ? "active" : "")"
                   id="@device.DeviceId"
                   @onclick="@(async () => await SelectCamera(device.DeviceId))">
                    @device.Label
                </a>
            }
        }
    </div>
</div>

Le balisage du composant Cameras inclut un menu déroulant qui permet à l'utilisateur de sélectionner une caméra. Lorsque le composant est initialisé, les périphériques sont extraits de la fonctionnalité d'opérateur intermédiaire JavaScript encapsulée dans la classe VideoJS. Lorsqu'une caméra est sélectionnée, son identifiant de périphérique est utilisé pour lancer l'aperçu de la caméra en direct.

Si vous vous souvenez de la page Index, le composant Cameras était positionné au-dessus des commandes de disposition et d'entrée des salles. Lorsque l'application est en cours d'exécution, elle ressemble à ceci :

Capture d&#x27;écran Composant Cameras (Caméras)

Un monde sans JavaScript

Bien que ce soit un en-tête digne d'un clickbait, ce n'est pas le cas. Voici le code JavaScript adoré de WebAssembly et pour lequel la classe d'opérateur intermédiaire VideoJS transporte des mots doux entre lui et le code WebAssembly.

Ajoutez le code JavaScript suivant dans le fichier Client/wwwroot/site.js :

let _videoTrack = null;
let _activeRoom = null;
let _participants = new Map();
let _dominantSpeaker = null;

async function getVideoDevices() {
    try {
        let devices = await navigator.mediaDevices.enumerateDevices();
        if (devices.every(d => !d.label)) {
            await navigator.mediaDevices.getUserMedia({
                video: true
            });
        }

        devices = await navigator.mediaDevices.enumerateDevices();
        if (devices && devices.length) {
            const deviceResults = [];
            devices.filter(device => device.kind === 'videoinput')
                .forEach(device => {
                    const { deviceId, label } = device;
                    deviceResults.push({ deviceId, label });
                });

            return deviceResults;
        }
    } catch (error) {
        console.log(error);
    }

    return [];
}

async function startVideo(deviceId, selector) {
    const cameraContainer = document.querySelector(selector);
    if (!cameraContainer) {
        return;
    }

    try {
        if (_videoTrack) {
            _videoTrack.detach().forEach(element => element.remove());
        }

        _videoTrack = await Twilio.Video.createLocalVideoTrack({ deviceId });
        const videoEl = _videoTrack.attach();
        cameraContainer.append(videoEl);
    } catch (error) {
        console.log(error);
    }
}

async function createOrJoinRoom(roomName, token) {
    try {
        if (_activeRoom) {
            _activeRoom.disconnect();
        }

        const audioTrack = await Twilio.Video.createLocalAudioTrack();
        const tracks = [audioTrack, _videoTrack];
        _activeRoom = await Twilio.Video.connect(
            token, {
            name: roomName,
            tracks,
            dominantSpeaker: true
        });

        if (_activeRoom) {
            initialize(_activeRoom.participants);
            _activeRoom
                .on('disconnected',
                    room => room.localParticipant.tracks.forEach(
                        publication => detachTrack(publication.track)))
                .on('participantConnected', participant => add(participant))
                .on('participantDisconnected', participant => remove(participant))
                .on('dominantSpeakerChanged', dominantSpeaker => loudest(dominantSpeaker));
        }
    } catch (error) {
        console.error(`Unable to connect to Room: ${error.message}`);
    }

    return !!_activeRoom;
}

function initialize(participants) {
    _participants = participants;
    if (_participants) {
        _participants.forEach(participant => registerParticipantEvents(participant));
    }
}

function add(participant) {
    if (_participants && participant) {
        _participants.set(participant.sid, participant);
        registerParticipantEvents(participant);
    }
}

function remove(participant) {
    if (_participants && _participants.has(participant.sid)) {
        _participants.delete(participant.sid);
    }
}

function loudest(participant) {
    _dominantSpeaker = participant;
}

function registerParticipantEvents(participant) {
    if (participant) {
        participant.tracks.forEach(publication => subscribe(publication));
        participant.on('trackPublished', publication => subscribe(publication));
        participant.on('trackUnpublished',
            publication => {
                if (publication && publication.track) {
                    detachRemoteTrack(publication.track);
                }
            });
    }
}

function subscribe(publication) {
    if (isMemberDefined(publication, 'on')) {
        publication.on('subscribed', track => attachTrack(track));
        publication.on('unsubscribed', track => detachTrack(track));
    }
}

function attachTrack(track) {
    if (isMemberDefined(track, 'attach')) {
        const audioOrVideo = track.attach();
        audioOrVideo.id = track.sid;

        if ('video' === audioOrVideo.tagName.toLowerCase()) {
            const responsiveDiv = document.createElement('div');
            responsiveDiv.id = track.sid;
            responsiveDiv.classList.add('embed-responsive');
            responsiveDiv.classList.add('embed-responsive-16by9');

            const responsiveItem = document.createElement('div');
            responsiveItem.classList.add('embed-responsive-item');

            // Similar to.
            // <div class="embed-responsive embed-responsive-16by9">
            //   <div id="camera" class="embed-responsive-item">
            //     <video></video>
            //   </div>
            // </div>
            responsiveItem.appendChild(audioOrVideo);
            responsiveDiv.appendChild(responsiveItem);
            document.getElementById('participants').appendChild(responsiveDiv);
        } else {
            document.getElementById('participants')
                .appendChild(audioOrVideo);
        }
    }
}

function detachTrack(track) {
    if (this.isMemberDefined(track, 'detach')) {
        track.detach()
            .forEach(el => {
                if ('video' === el.tagName.toLowerCase()) {
                    const parent = el.parentElement;
                    if (parent && parent.id !== 'camera') {
                        const grandParent = parent.parentElement;
                        if (grandParent) {
                            grandParent.remove();
                        }
                    }
                } else {
                    el.remove()
                }
            });
    }
}

function isMemberDefined(instance, member) {
    return !!instance && instance[member] !== undefined;
}

async function leaveRoom() {
    try {
        if (_activeRoom) {
            _activeRoom.disconnect();
            _activeRoom = null;
        }

        if (_participants) {
            _participants.clear();
        }
    }
    catch (error) {
        console.error(error);
    }
}

window.videoInterop = {
    getVideoDevices,
    startVideo,
    createOrJoinRoom,
    leaveRoom
};

Vous pouvez voir certains messages d'avertissement ESLint concernant les propriétés _videoTrack et _activeRoom. L'application peut appeler createOrJoinRoom uniquement s'il y a un _videoTrack local. Lorsque cet appel est effectué, il est impossible d'effectuer l'autre en même temps. Vous pouvez donc ignorer l'avertissement.

Le fichier JavaScript .site.js fournit un certain nombre de fonctions clés :

  • Exposition de périphériques vidéo
  • Lancement de l'aperçu vidéo
  • Création d'une salle ou accès à une salle
  • Départ d'une salle

Toutes les fonctionnalités sont exposées par window.videoInterop. Les quatre fonctions sont représentées par l'élément littéral de l'objet, et l'état ne nécessite que peu de maintenance.

Exposition de périphériques vidéo

La fonction getVideoDevices demande de manière asynchrone au navigator.mediaDevices d'obtenir enumerableDevices.

Si la liste des périphériques renvoyée est vide, cela signifie que la fonction doit demander à l'utilisateur l'autorisation d'utiliser sa webcam et son microphone. La fonction demande explicitement l'élément multimédia de l'utilisateur comme suit :

await navigator.mediaDevices.getUserMedia({
    video: true
});

L'utilisateur est invité à fournir l'autorisation dans le cadre de getUserMedia, puis les périphériques sont énumérés une fois de plus pour remplir la liste devices. Le tableau de périphériques est ensuite renvoyé à l'appelant Blazor WebAssembly. Les résultats sont convertis en série à partir d'un tableau d'objets littéraux JavaScript ayant des membres deviceId et label vers un tableau de la classe C# Device.

Lancement de l'aperçu vidéo

La fonction startVideo accepte le deviceId que l'utilisateur a sélectionné dans le composant Camera, ainsi qu'un selector qui représente l'identifiant de l'élément du conteneur de caméra. S'il y avait un précédent élément _videoTrack, il est détaché et recréé à l'aide de la fonction Twilio.Video.createLocalVideoTrack. Il est ensuite attaché et ajouté au conteneur de la caméra. Cela servira de flux de prévisualisation vidéo local du client.

 

Création d'une salle ou accès à une salle

La fonction createOrJoinRoom prend un roomName et un token. Le token est l'élément TwilioJwt qui est résolu à partir du point de terminaison api/twilio/token du serveur. Avec ces deux arguments, et l'élément _videoTrack en contexte, la fonction peut appeler Twilio.Video.createLocalAudioTrack pour obtenir tout ce dont elle a besoin pour se connecter à la salle.

Le token et un objet d'options contenant les pistes audio et vidéo locales, ainsi que le nom de la salle sont utilisés pour appeler Twilio.Video.Connect. La fonction connect renvoie l'élément _activeRoom et expose un certain nombre d'événements. La table suivante fournit une liste complète de chaque événement associé à une ressource SDK :

Inscription de l'événement

Description

_activeRoom.on('disconnected', room => { });

Se produit lorsqu'un utilisateur quitte la salle

_activeRoom.on('participantConnected', participant => { });

Se produit lorsqu'un nouveau participant rejoint la salle

_activeRoom.on('participantDisconnected', participant => { });

Se produit lorsqu'un participant quitte la salle

participant.on('trackPublished', publication => { });

Se produit lorsqu'une publication de piste est publiée

participant.on('trackUnpublished', publication => { });

Se produit lorsque la publication de piste est annulée

publication.on('subscribed', track => { });

Se produit lorsqu'une piste est souscrite

publication.on('unsubscribed', track => { });

Se produit lorsque la souscription de piste est annulée

Départ d'une salle

La fonction leaveRoom vérifie si l'élément _activeRoom est true et appelle disconnect pour mettre fin à l'appel. De plus, la liste des_participants est effacée si elle était également true. Le nettoyage de toutes les ressources s'effectue implicitement.

Assemblage et test de l'application terminée

Ouf, c'était un sacré projet ! Vous avez terminé cette construction. Bravo !

Vous pouvez maintenant exécuter l'application et effectuer tout débogage nécessaire.

Configuration de votre environnement de test

Si vous utilisez Visual Studio 2019, vous pouvez apporter quelques modifications pour que vos tests s'exécutent plus facilement :

  • Définissez le projet Blazor.Twilio.Video.Server comme projet de démarrage.
  • Modifiez l'hôte du serveur Web en passant d'IIS Express au serveur Web Kestrel. Pour ce faire, dans la liste déroulante en regard de la flèche verte Run (Exécuter), remplacez IIS Express par Blazor.Twilio.Video.Server.

L'utilisation du serveur Web Kestrel ouvre une fenêtre de console qui vous fournit des informations de débogage supplémentaires. Cela vous permet aussi d'exécuter des applications Web ASP.NET Core sur MacOS et Linux, en plus de Windows.

Les paramètres d'exécution de l'application sont disponibles dans :

Blazor.Twilio.Video/Server/Properties/launchSettings.json

Si vous utilisez Visual Studio Code, assurez-vous que :

Les paramètres d'exécution de l'application sont disponibles dans :

Blazor.Twilio.Video/.vscode/launch.json

Test de l'application de chat vidéo

Exécutez l'application.

Si vous utilisez Visual Studio 2019, l'application doit ouvrir le navigateur que vous avez sélectionné et accéder à https://localhost:5001.

Si vous utilisez Visual Studio Code, ouvrez un navigateur et accédez à : https://localhost:5001 (ou au port que vous avez configuré) avec votre navigateur.

Vous devriez voir un message « Loading… » (Chargement...) de grande taille.

Une fois l'application chargée, votre navigateur vous invite à autoriser l'accès à la caméra : octroyez cette autorisation.

Sélectionnez un périphérique vidéo dans la liste de l'interface utilisateur, comme illustré ci-dessous :

Capture d&#x27;écran Navigateur Web

Une fois que vous avez sélectionné un périphérique vidéo dans l'application, votre navigateur peut vous inviter à sélectionner un périphérique audio.

Si vous disposez de deux sources vidéo sur votre ordinateur, ouvrez une fenêtre de navigation privée ou un navigateur différent, et sélectionnez un périphérique vidéo différent de celui que vous avez sélectionné dans la première fenêtre du navigateur.

Remarque : Chrome et Firefox gèrent les périphériques vidéo un peu différemment. Le comportement observé peut donc être légèrement différent selon le ou les navigateurs que vous utilisez.

Nommez une salle de chat vidéo, appuyez sur la touche Tab, puis cliquez sur Create (Créer). Vous devriez voir le nom de la salle ajouté à la liste des salles, ainsi que le nombre de participants actuels et le nombre maximal de participants.

Lorsqu'une salle est créée, l'aperçu local est déplacé juste sous les paramètres de façon à ce que les participants de la salle distante qui rejoignent voient leur flux vidéo dans la plus grande zone de visualisation.

Remarque : le système de grille dans le CSS pour cette application est configuré pour une grille 4 x 4. L'ajout de plus de deux participants rendra probablement la mise en page plus étrange.

Vous trouverez ci-dessous une capture d'écran montrant le test de l'application finale avec deux navigateurs empilés l'un sur l'autre. Vous remarquez la vue du créateur de la salle en haut et en dessous de celle-ci, la vue du deuxième « participant » avec l'ordre des vues inversé.  

Image de l&#x27;application en cours d&#x27;exécution

Note de l'auteur : je porte un t-shirt Twilio, mes tatouages rendent bien et je peux avoir une conversation avec moi-même.

Note de l'éditeur : une journée de travail ordinaire...

Résumé

Ce post vous a montré comment construire une application de chat vidéo entièrement fonctionnelle avec ASP.NET Core Blazor WebAssembly, SignalR et Twilio Programmable Video. Le SDK Twilio .NET fournit des JWT au code front-end Blazor côté client ainsi que des informations sur les salles via l'API Web ASP.NET Core. La SPA Blazor côté client intègre le SDK JavaScript Twilio.

Ressources supplémentaires

Le répertoire compagnon de GitHub contient des sélections permanentes à inclure éventuellement dans votre application de production.

Pour en savoir plus sur les outils et technologies utilisés dans ce tutoriel, consultez les ressources techniques suivantes sur docs.microsoft.com, la référence standard en matière de documentation technique :

Pour découvrir comment implémenter le chat vidéo avec ASP.NET Core et un front-end Angular, consultez ce post sur le blog de Twilio, ainsi que son post associé sur le déploiement et l'exécution de l'application sur Azure :

Construction d'une application de chat vidéo avec ASP.NET Core 3.1, Angular 9 et Twilio

Exécution d'une application de chat vidéo construite avec ASP.NET Core 3.1 et Twilio Programmable Video sur Microsoft Azure

Consultez également l'apparition de Corey Weathers, Twilio Developer Evangelist, sur le salon .NET Docs pour découvrir d'autres « pépites » !

David Pine est un double MVP MicrosoftGoogle Developer ExpertChampion Twilio et conférencier international. Vous pouvez le suivre sur Twitter @davidine7. Pensez à consulter son blog sur https://davidpine.net.