Usare i webhook di Azure per monitorare le notifiche dei processi di Servizi multimediali con .NET

Quando si esegue un processo, spesso è necessario monitorarne l'avanzamento. È possibile monitorare le notifiche dei processi di Servizi multimediali tramite webhook di Azure o archiviazione code di Azure. Questo argomento illustra come usare i webhook.

Questo argomento illustra come

  • Definire una funzione di Azure personalizzata per rispondere ai webhook.

    In questo caso, il webhook viene attivato da Servizi multimediali quando il processo di codifica cambia stato. La funzione è in attesa del callback del webhook dalle notifiche di Servizi multimediali e pubblica l'asset di output al termine del processo.

    Nota

    Prima di continuare, assicurarsi di comprendere come funzionano le associazioni HTTP e webhook in Funzioni di Azure.

  • Aggiungere un webhook all'attività di codifica e specificarne l'URL e la chiave privata a cui risponde. Alla fine dell'argomento viene fornito un esempio per l'aggiunta di un webhook all'attività di codifica.

È possibile trovare qui le definizioni di varie funzioni di Servizi multimediali di Azure .NET (incluso quella mostrata in questo argomento).

Prerequisiti

Per completare l'esercitazione è necessario quanto segue:

Creare un'app per le funzioni

  1. Passare al portale di Azure e accedere con il proprio account Azure.
  2. Creare un'app per le funzioni come descritto qui.

Configurare le impostazioni dell'app per le funzioni

Quando si sviluppano le funzioni di Servizi multimediali, è utile aggiungere variabili di ambiente che verranno usati nelle funzioni. Per configurare le impostazioni dell'app, fare clic sul collegamento Configurare le impostazioni dell'app.

La sezione relativa alle impostazioni dell'applicazione indica i parametri usati nel webhook definito in questo argomento. Aggiungere anche i parametri seguenti alle impostazioni dell'app.

Nome Definizione Esempio
SigningKey Chiave di firma. j0txf1f8msjytzvpe40nxbpxdcxtqcgxy0nt
WebHookEndpoint Indirizzo di un endpoint di webhook. Dopo avere creato la funzione del webhook, è anche possibile copiare l'URL dal collegamento Recupera URL della funzione. https://juliakofuncapp.azurewebsites.net/api/Notification_Webhook_Function?code=iN2phdrTnCxmvaKExFWOTulfnm4C71mMLIy8tzLr7Zvf6Z22HHIK5g==.

Creare una funzione

In seguito alla distribuzione dell'app per le funzioni, questa verrà visualizzata tra le Funzioni di Azure dei Servizi app.

  1. Selezionare l'app per le funzioni e fare clic su Nuova funzione.
  2. Selezionare il codice C# e lo scenario API e webhook.
  3. Selezionare Generic Webhook - C# (Webhook generico - C#).
  4. Assegnare un nome al webhook e premere Crea.

File

La funzione di Azure viene associata ai file di codice e agli altri file descritti in questa sezione. Per impostazione predefinita, una funzione è associata ai file function.json e run.csx (C#). Sarà necessario aggiungere un file project.json. La parte successiva di questa sezione illustra le definizioni per questi file.

input

function.json

Il file function.json definisce le associazioni di funzione e altre impostazioni di configurazione. Il runtime usa questo file per determinare gli eventi da monitorare e come passare i dati e restituirli dall'esecuzione di funzioni.

{
  "bindings": [
    {
      "type": "httpTrigger",
      "direction": "in",
      "webHookType": "genericJson",
      "name": "req"
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    }
  ],
  "disabled": false
}

project.json

Il file project.json contiene dipendenze.

{
  "frameworks": {
    "net46":{
      "dependencies": {
        "windowsazure.mediaservices": "4.0.0.4",
        "windowsazure.mediaservices.extensions": "4.0.0.4",
        "Microsoft.IdentityModel.Clients.ActiveDirectory": "3.13.1",
        "Microsoft.IdentityModel.Protocol.Extensions": "1.0.2.206221351"
      }
    }
   }
}

run.csx

Il codice in questa sezione mostra un'implementazione di una funzione di Azure rappresentata da un webhook. In questo esempio la funzione è in attesa del callback del webhook dalle notifiche di Servizi multimediali e pubblica l'asset di output al termine del processo.

Il webhook prevede una chiave di firma (credenziale) che corrisponda a quella trasmessa quando si configura l'endpoint di notifica. La chiave di firma è il valore codificato di 64 byte Base64 usato per proteggere i callback dei webhook da Servizi multimediali di Azure.

Nel codice di definizione del webhook che segue, il metodo VerifyWebHookRequestSignature esegue la verifica del messaggio di notifica. Lo scopo di questa convalida consiste nel garantire che il messaggio sia stato inviato da Servizi multimediali di Microsoft Azure e non sia stato manomesso. La firma è facoltativa per Funzioni di Azure in quanto il valore Codice è impostato come un parametro di query su Transport Layer Security (TLS).

Nota

È previsto un limite di 1.000.000 di criteri per i diversi criteri AMS (ad esempio per i criteri Locator o ContentKeyAuthorizationPolicy). Usare lo stesso ID criterio se si usano sempre gli stessi giorni/autorizzazioni di accesso, come nel cado di criteri per i localizzatori che devono rimanere attivi per molto tempo (criteri di non caricamento). Per altre informazioni, vedere questo argomento.

///////////////////////////////////////////////////
#r "Newtonsoft.Json"

using System;
using Microsoft.WindowsAzure.MediaServices.Client;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.IO;
using System.Globalization;
using Newtonsoft.Json;
using Microsoft.Azure;
using System.Net;
using System.Security.Cryptography;
using Microsoft.Azure.WebJobs;
using Microsoft.IdentityModel.Clients.ActiveDirectory;

internal const string SignatureHeaderKey = "sha256";
internal const string SignatureHeaderValueTemplate = SignatureHeaderKey + "={0}";
static string _webHookEndpoint = Environment.GetEnvironmentVariable("WebHookEndpoint");
static string _signingKey = Environment.GetEnvironmentVariable("SigningKey");

static readonly string _AADTenantDomain = Environment.GetEnvironmentVariable("AMSAADTenantDomain");
static readonly string _RESTAPIEndpoint = Environment.GetEnvironmentVariable("AMSRESTAPIEndpoint");

static readonly string _AMSClientId = Environment.GetEnvironmentVariable("AMSClientId");
static readonly string _AMSClientSecret = Environment.GetEnvironmentVariable("AMSClientSecret");

static CloudMediaContext _context = null;

public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)
{
    log.Info($"C# HTTP trigger function processed a request. RequestUri={req.RequestUri}");

    Task<byte[]> taskForRequestBody = req.Content.ReadAsByteArrayAsync();
    byte[] requestBody = await taskForRequestBody;

    string jsonContent = await req.Content.ReadAsStringAsync();
    log.Info($"Request Body = {jsonContent}");

    IEnumerable<string> values = null;
    if (req.Headers.TryGetValues("ms-signature", out values))
    {
        byte[] signingKey = Convert.FromBase64String(_signingKey);
        string signatureFromHeader = values.FirstOrDefault();

        if (VerifyWebHookRequestSignature(requestBody, signatureFromHeader, signingKey))
        {
            string requestMessageContents = Encoding.UTF8.GetString(requestBody);

            NotificationMessage msg = JsonConvert.DeserializeObject<NotificationMessage>(requestMessageContents);

            if (VerifyHeaders(req, msg, log))
            { 
                string newJobStateStr = (string)msg.Properties.Where(j => j.Key == "NewState").FirstOrDefault().Value;
                if (newJobStateStr == "Finished")
                {
                    AzureAdTokenCredentials tokenCredentials = new AzureAdTokenCredentials(_AADTenantDomain,
                                new AzureAdClientSymmetricKey(_AMSClientId, _AMSClientSecret),
                                AzureEnvironments.AzureCloudEnvironment);

                    AzureAdTokenProvider tokenProvider = new AzureAdTokenProvider(tokenCredentials);

                    _context = new CloudMediaContext(new Uri(_RESTAPIEndpoint), tokenProvider);

                    if(_context!=null)   
                    {                        
                        string urlForClientStreaming = PublishAndBuildStreamingURLs(msg.Properties["JobId"]);
                        log.Info($"URL to the manifest for client streaming using HLS protocol: {urlForClientStreaming}");
                    }
                }

                return req.CreateResponse(HttpStatusCode.OK, string.Empty);
            }
            else
            {
                log.Info($"VerifyHeaders failed.");
                return req.CreateResponse(HttpStatusCode.BadRequest, "VerifyHeaders failed.");
            }
        }
        else
        {
            log.Info($"VerifyWebHookRequestSignature failed.");
            return req.CreateResponse(HttpStatusCode.BadRequest, "VerifyWebHookRequestSignature failed.");
        }
    }

    return req.CreateResponse(HttpStatusCode.BadRequest, "Generic Error.");
}

private static string PublishAndBuildStreamingURLs(String jobID)
{
    IJob job = _context.Jobs.Where(j => j.Id == jobID).FirstOrDefault();
    IAsset asset = job.OutputMediaAssets.FirstOrDefault();

    // Create a 30-day readonly access policy. 
    // You cannot create a streaming locator using an AccessPolicy that includes write or delete permissions.
    IAccessPolicy policy = _context.AccessPolicies.Create("Streaming policy",
    TimeSpan.FromDays(30),
    AccessPermissions.Read);

    // Create a locator to the streaming content on an origin. 
    ILocator originLocator = _context.Locators.CreateLocator(LocatorType.OnDemandOrigin, asset,
    policy,
    DateTime.UtcNow.AddMinutes(-5));

    // Get a reference to the streaming manifest file from the  
    // collection of files in the asset. 
    var manifestFile = asset.AssetFiles.Where(f => f.Name.ToLower().
                EndsWith(".ism")).
                FirstOrDefault();

    // Create a full URL to the manifest file. Use this for playback
    // in streaming media clients. 
    string urlForClientStreaming = originLocator.Path + manifestFile.Name + "/manifest" +  "(format=m3u8-aapl)";
    return urlForClientStreaming;

}

private static bool VerifyWebHookRequestSignature(byte[] data, string actualValue, byte[] verificationKey)
{
    using (var hasher = new HMACSHA256(verificationKey))
    {
        byte[] sha256 = hasher.ComputeHash(data);
        string expectedValue = string.Format(CultureInfo.InvariantCulture, SignatureHeaderValueTemplate, ToHex(sha256));

        return (0 == String.Compare(actualValue, expectedValue, System.StringComparison.Ordinal));
    }
}

private static bool VerifyHeaders(HttpRequestMessage req, NotificationMessage msg, TraceWriter log)
{
    bool headersVerified = false;

    try
    {
        IEnumerable<string> values = null;
        if (req.Headers.TryGetValues("ms-mediaservices-accountid", out values))
        {
            string accountIdHeader = values.FirstOrDefault();
            string accountIdFromMessage = msg.Properties["AccountId"];

            if (0 == string.Compare(accountIdHeader, accountIdFromMessage, StringComparison.OrdinalIgnoreCase))
            {
                headersVerified = true;
            }
            else
            {
                log.Info($"accountIdHeader={accountIdHeader} does not match accountIdFromMessage={accountIdFromMessage}");
            }
        }
        else
        {
            log.Info($"Header ms-mediaservices-accountid not found.");
        }
    }
    catch (Exception e)
    {
        log.Info($"VerifyHeaders hit exception {e}");
        headersVerified = false;
    }

    return headersVerified;
}

private static readonly char[] HexLookup = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };

/// <summary>
/// Converts a <see cref="T:byte[]"/> to a hex-encoded string.
/// </summary>
private static string ToHex(byte[] data)
{
    if (data == null)
    {
        return string.Empty;
    }

    char[] content = new char[data.Length * 2];
    int output = 0;
    byte d;

    for (int input = 0; input < data.Length; input++)
    {
        d = data[input];
        content[output++] = HexLookup[d / 0x10];
        content[output++] = HexLookup[d % 0x10];
    }

    return new string(content);
}

internal enum NotificationEventType
{
    None = 0,
    JobStateChange = 1,
    NotificationEndPointRegistration = 2,
    NotificationEndPointUnregistration = 3,
    TaskStateChange = 4,
    TaskProgress = 5
}

internal sealed class NotificationMessage
{
    public string MessageVersion { get; set; }
    public string ETag { get; set; }
    public NotificationEventType EventType { get; set; }
    public DateTime TimeStamp { get; set; }
    public IDictionary<string, string> Properties { get; set; }
}

Salvare ed eseguire la funzione.

Output della funzione

Dopo aver attivato il webhook, l'esempio precedente produce l'output seguente (i valori potrebbero essere differenti).

C# HTTP trigger function processed a request. RequestUri=https://juliako001-functions.azurewebsites.net/api/Notification_Webhook_Function?code=9376d69kygoy49oft81nel8frty5cme8hb9xsjslxjhalwhfrqd79awz8ic4ieku74dvkdfgvi
Request Body = {
  "MessageVersion": "1.1",
  "ETag": "b8977308f48858a8f224708bc963e1a09ff917ce730316b4e7ae9137f78f3b20",
  "EventType": 4,
  "TimeStamp": "2017-02-16T03:59:53.3041122Z",
  "Properties": {
    "JobId": "nb:jid:UUID:badd996c-8d7c-4ae0-9bc1-bd7f1902dbdd",
    "TaskId": "nb:tid:UUID:80e26fb9-ee04-4739-abd8-2555dc24639f",
    "NewState": "Finished",
    "OldState": "Processing",
    "AccountName": "mediapkeewmg5c3peq",
    "AccountId": "301912b0-659e-47e0-9bc4-6973f2be3424",
    "NotificationEndPointId": "nb:nepid:UUID:cb5d707b-4db8-45fe-a558-19f8d3306093"
  }
}

URL to the manifest for client streaming using HLS protocol: http://mediapkeewmg5c3peq.streaming.mediaservices.windows.net/0ac98077-2b58-4db7-a8da-789a13ac6167/BigBuckBunny.ism/manifest(format=m3u8-aapl)

Aggiungere un all'attività di codifica

Questa sezione mostra il codice che aggiunge una notifica di webhook a un'attività. È inoltre possibile aggiungere una notifica di livello di processo, che dovrebbe essere più utile per un processo con attività concatenate.

  1. Creare una nuova applicazione console C# in Visual Studio. Immettere un valore nei campi Nome, Percorso e Nome soluzione, quindi fare clic su OK.
  2. Usare NuGet per installare Servizi multimediali di Azure.
  3. Aggiornare il file App.config con valori appropriati:

    • Informazioni sulla connessione a Servizi multimediali di Azure,
    • URL del webhook che prevede di ricevere le notifiche,
    • la chiave di firma corrispondente alla chiave attesa dal webhook. La chiave di firma è il valore codificato di 64 byte Base64 usato per proteggere i callback dei webhook da Servizi multimediali di Azure.

        <appSettings>
          <add key="AMSAADTenantDomain" value="domain" />
          <add key="AMSRESTAPIEndpoint" value="endpoint" />
      
          <add key="AMSClientId" value="clinet id" />
          <add key="AMSClientSecret" value="client secret" />
      
          <add key="WebhookURL" value="https://yourapp.azurewebsites.net/api/functionname?code=ApiKey" />
          <add key="WebhookSigningKey" value="j0txf1f8msjytzvpe40nxbpxdcxtqcgxy0nt" />
        </appSettings>
      
  4. Aggiornare il file Program.cs con il codice seguente:

     using System;
     using System.Configuration;
     using System.Linq;
     using Microsoft.WindowsAzure.MediaServices.Client;
    
     namespace NotificationWebHook
     {
         class Program
         {
         // Read values from the App.config file.
         private static readonly string _AMSAADTenantDomain =
             ConfigurationManager.AppSettings["AMSAADTenantDomain"];
         private static readonly string _AMSRESTAPIEndpoint =
             ConfigurationManager.AppSettings["AMSRESTAPIEndpoint"];
    
         private static readonly string _AMSClientId =
             ConfigurationManager.AppSettings["AMSClientId"];
         private static readonly string _AMSClientSecret =
             ConfigurationManager.AppSettings["AMSClientSecret"];
    
         private static readonly string _webHookEndpoint =
             ConfigurationManager.AppSettings["WebhookURL"];
         private static readonly string _signingKey =
              ConfigurationManager.AppSettings["WebhookSigningKey"];
    
         // Field for service context.
         private static CloudMediaContext _context = null;
    
         static void Main(string[] args)
         {
             AzureAdTokenCredentials tokenCredentials = new AzureAdTokenCredentials(_AMSAADTenantDomain,
                 new AzureAdClientSymmetricKey(_AMSClientId, _AMSClientSecret),
                 AzureEnvironments.AzureCloudEnvironment);
    
             AzureAdTokenProvider tokenProvider = new AzureAdTokenProvider(tokenCredentials);
    
             _context = new CloudMediaContext(new Uri(_AMSRESTAPIEndpoint), tokenProvider);
    
             byte[] keyBytes = Convert.FromBase64String(_signingKey);
    
             IAsset newAsset = _context.Assets.FirstOrDefault();
    
             // Check for existing Notification Endpoint with the name "FunctionWebHook"
    
             var existingEndpoint = _context.NotificationEndPoints.Where(e => e.Name == "FunctionWebHook").FirstOrDefault();
             INotificationEndPoint endpoint = null;
    
             if (existingEndpoint != null)
             {
             Console.WriteLine("webhook endpoint already exists");
             endpoint = (INotificationEndPoint)existingEndpoint;
             }
             else
             {
             endpoint = _context.NotificationEndPoints.Create("FunctionWebHook",
                 NotificationEndPointType.WebHook, _webHookEndpoint, keyBytes);
             Console.WriteLine("Notification Endpoint Created with Key : {0}", keyBytes.ToString());
             }
    
             // Declare a new encoding job with the Standard encoder
             IJob job = _context.Jobs.Create("MES Job");
    
             // Get a media processor reference, and pass to it the name of the 
             // processor to use for the specific task.
             IMediaProcessor processor = GetLatestMediaProcessorByName("Media Encoder Standard");
    
             ITask task = job.Tasks.AddNew("My encoding task",
             processor,
             "Adaptive Streaming",
             TaskOptions.None);
    
             // Specify the input asset to be encoded.
             task.InputAssets.Add(newAsset);
    
             // Add an output asset to contain the results of the job. 
             // This output is specified as AssetCreationOptions.None, which 
             // means the output asset is not encrypted. 
             task.OutputAssets.AddNew(newAsset.Name, AssetCreationOptions.None);
    
             // Add the WebHook notification to this Task and request all notification state changes.
             // Note that you can also add a job level notification
             // which would be more useful for a job with chained tasks.  
             if (endpoint != null)
             {
             task.TaskNotificationSubscriptions.AddNew(NotificationJobState.All, endpoint, true);
             Console.WriteLine("Created Notification Subscription for endpoint: {0}", _webHookEndpoint);
             }
             else
             {
             Console.WriteLine("No Notification Endpoint is being used");
             }
    
             job.Submit();
    
             Console.WriteLine("Expect WebHook to be triggered for the Job ID: {0}", job.Id);
             Console.WriteLine("Expect WebHook to be triggered for the Task ID: {0}", task.Id);
    
             Console.WriteLine("Job Submitted");
    
         }
         private static IMediaProcessor GetLatestMediaProcessorByName(string mediaProcessorName)
         {
             var processor = _context.MediaProcessors.Where(p => p.Name == mediaProcessorName).
             ToList().OrderBy(p => new Version(p.Version)).LastOrDefault();
    
             if (processor == null)
             throw new ArgumentException(string.Format("Unknown media processor", mediaProcessorName));
    
             return processor;
         }
         }
     }
    

Passaggi successivi

Altre informazioni sui percorsi di apprendimento di Servizi multimediali di Azure:

Fornire commenti e suggerimenti

Usare il forum di suggerimenti degli utenti per fornire commenti e suggerimenti su come migliorare Servizi multimediali di Azure. È anche possibile passare direttamente a una delle categorie seguenti: