Usare i webhook di Azure per monitorare le notifiche dei processi di Servizi multimediali con .NETUse Azure Webhooks to monitor Media Services job notifications with .NET

Quando si esegue un processo, spesso è necessario monitorarne l'avanzamento.When you run jobs, you often require a way to track job progress. È possibile monitorare le notifiche dei processi di Servizi multimediali tramite webhook di Azure o archiviazione code di Azure.You can monitor Media Services job notifications by using Azure Webhooks or Azure Queue storage. Questo argomento illustra come usare i webhook.This topic shows how to work with webhooks.

Questo argomento illustra comeThis topic shows how to

  • Definire una funzione di Azure personalizzata per rispondere ai webhook.Define an Azure Function that is customized to respond to webhooks.

    In questo caso, il webhook viene attivato da Servizi multimediali quando il processo di codifica cambia stato.In this case, the webhook is triggered by Media Services when your encoding job changes status. La funzione è in attesa del callback del webhook dalle notifiche di Servizi multimediali e pubblica l'asset di output al termine del processo.The function listens for the webhook call back from Media Services notifications and publishes the output asset once the job finishes.

    Nota

    Prima di continuare, assicurarsi di comprendere come funzionano le associazioni HTTP e webhook in Funzioni di Azure.Before continuing, make sure you understand how Azure Functions HTTP and webhook bindings work.

  • Aggiungere un webhook all'attività di codifica e specificarne l'URL e la chiave privata a cui risponde.Add a webhook to your encoding task and specify the webhook URL and secret key that this webhook responds to. Alla fine dell'argomento viene fornito un esempio per l'aggiunta di un webhook all'attività di codifica.You will find an example that adds a webhook to your encoding task at the end of the topic.

È possibile trovare qui le definizioni di varie funzioni di Servizi multimediali di Azure .NET (incluso quella mostrata in questo argomento).You can find definitions of various Media Services .NET Azure Functions (including the one shown in this topic) here.

PrerequisitiPrerequisites

Per completare l'esercitazione è necessario quanto segue:The following are required to complete the tutorial:

Creare un'app per le funzioniCreate a function app

  1. Passare al portale di Azure e accedere con il proprio account Azure.Go to the Azure portal and sign-in with your Azure account.
  2. Creare un'app per le funzioni come descritto qui.Create a function app as described here.

Configurare le impostazioni dell'app per le funzioniConfigure function app settings

Quando si sviluppano le funzioni di Servizi multimediali, è utile aggiungere variabili di ambiente che verranno usati nelle funzioni.When developing Media Services functions, it is handy to add environment variables that will be used throughout your functions. Per configurare le impostazioni dell'app, fare clic sul collegamento Configurare le impostazioni dell'app.To configure app settings, click the Configure App Settings link.

La sezione relativa alle impostazioni dell'applicazione indica i parametri usati nel webhook definito in questo argomento.The application settings section defines parameters that are used in the webhook defined in this topic. Aggiungere anche i parametri seguenti alle impostazioni dell'app.Also add the following parameters to the app settings.

NomeName DefinizioneDefinition EsempioExample
SigningKeySigningKey Chiave di firma.A signing key. j0txf1f8msjytzvpe40nxbpxdcxtqcgxy0ntj0txf1f8msjytzvpe40nxbpxdcxtqcgxy0nt
WebHookEndpointWebHookEndpoint Indirizzo di un endpoint di webhook.A webhook endpoint address. Dopo avere creato la funzione del webhook, è anche possibile copiare l'URL dal collegamento Recupera URL della funzione.Once your webhook function is created, you can copy the URL from the Get function URL link. https://juliakofuncapp.azurewebsites.net/api/Notification_Webhook_Function?code=iN2phdrTnCxmvaKExFWOTulfnm4C71mMLIy8tzLr7Zvf6Z22HHIK5g==.https://juliakofuncapp.azurewebsites.net/api/Notification_Webhook_Function?code=iN2phdrTnCxmvaKExFWOTulfnm4C71mMLIy8tzLr7Zvf6Z22HHIK5g==.

Creare una funzioneCreate a function

In seguito alla distribuzione dell'app per le funzioni, questa verrà visualizzata tra le Funzioni di Azure dei Servizi app.Once your function app is deployed, you can find it among App Services Azure Functions.

  1. Selezionare l'app per le funzioni e fare clic su Nuova funzione.Select your function app and click New Function.
  2. Selezionare il codice C# e lo scenario API e webhook.Select C# code and API & Webhooks scenario.
  3. Selezionare Generic Webhook - C# (Webhook generico - C#).Select Generic Webhook - C#.
  4. Assegnare un nome al webhook e premere Crea.Name your webhook and press Create.

FileFiles

La funzione di Azure viene associata ai file di codice e agli altri file descritti in questa sezione.Your Azure Function is associated with code files and other files that are described in this section. Per impostazione predefinita, una funzione è associata ai file function.json e run.csx (C#).By default, a function is associated with function.json and run.csx (C#) files. Sarà necessario aggiungere un file project.json.You will need to add a project.json file. La parte successiva di questa sezione illustra le definizioni per questi file.The rest of this section shows the definitions for these files.

input

function.jsonfunction.json

Il file function.json definisce le associazioni di funzione e altre impostazioni di configurazione.The function.json file defines the function bindings and other configuration settings. Il runtime usa questo file per determinare gli eventi da monitorare e come passare i dati e restituirli dall'esecuzione di funzioni.The runtime uses this file to determine the events to monitor and how to pass data into and return data from function execution.

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

project.jsonproject.json

Il file project.json contiene dipendenze.The project.json file contains dependencies.

{
  "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.csxrun.csx

Il codice in questa sezione mostra un'implementazione di una funzione di Azure rappresentata da un webhook.The code in this section shows an implementation of an Azure Function that is a 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.In this sample, the function listens for the webhook call back from Media Services notifications and publishes the output asset once the job finishes.

Il webhook prevede una chiave di firma (credenziale) che corrisponda a quella trasmessa quando si configura l'endpoint di notifica.The webhook expects a signing key (credential) to match the one you pass when you configure the notification endpoint. La chiave di firma è il valore codificato di 64 byte Base64 usato per proteggere i callback dei webhook da Servizi multimediali di Azure.The signing key is the 64-byte Base64 encoded value that is used to protect and secure your WebHooks callbacks from Azure Media Services.

Nel codice di definizione del webhook che segue, il metodo VerifyWebHookRequestSignature esegue la verifica del messaggio di notifica.In the webhook definition code that follows, the VerifyWebHookRequestSignature method does the verification of the notification message. 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.The purpose of this validation is to ensure that the message was sent by Azure Media Services and hasn't been tampered with. La firma è facoltativa per Funzioni di Azure in quanto il valore Codice è impostato come un parametro di query su Transport Layer Security (TLS).The signature is optional for Azure Functions as it has the Code value as a query parameter over 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).There is a limit of 1,000,000 policies for different AMS policies (for example, for Locator policy or 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).You should use the same policy ID if you are always using the same days / access permissions, for example, policies for locators that are intended to remain in place for a long time (non-upload policies). Per altre informazioni, vedere questo argomento.For more information, see this topic.

///////////////////////////////////////////////////
#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.Save and run your function.

Output della funzioneFunction output

Dopo aver attivato il webhook, l'esempio precedente produce l'output seguente (i valori potrebbero essere differenti).Once the webhook is triggered, the example above produces the following output, your values will vary.

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 codificaAdd a webhook to your encoding task

Questa sezione mostra il codice che aggiunge una notifica di webhook a un'attività.In this section, the code that adds a webhook notification to a Task is shown. È inoltre possibile aggiungere una notifica di livello di processo, che dovrebbe essere più utile per un processo con attività concatenate.You can also add a job level notification, which would be more useful for a job with chained tasks.

  1. Creare una nuova applicazione console C# in Visual Studio.Create a new C# Console Application in Visual Studio. Immettere un valore nei campi Nome, Percorso e Nome soluzione, quindi fare clic su OK.Enter the Name, Location, and Solution name, and then click OK.
  2. Usare NuGet per installare Servizi multimediali di Azure.Use NuGet to install Azure Media Services.
  3. Aggiornare il file App.config con valori appropriati:Update App.config file with appropriate values:

    • Informazioni sulla connessione a Servizi multimediali di Azure,Azure Media Services connection information,
    • URL del webhook che prevede di ricevere le notifiche,webhook URL that expects to get the notifications,
    • la chiave di firma corrispondente alla chiave attesa dal webhook.the signing key that matches the key that your webhook expects. La chiave di firma è il valore codificato di 64 byte Base64 usato per proteggere i callback dei webhook da Servizi multimediali di Azure.The signing key is the 64-byte Base64 encoded value that is used to protect and secure your webhooks callbacks from Azure Media Services.

        <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:Update your Program.cs file with the following code:

     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 successiviNext steps

Altre informazioni sui percorsi di apprendimento di Servizi multimediali di Azure:Read about the Azure Media Services learning paths:

Fornire commenti e suggerimentiProvide feedback

Usare il forum di suggerimenti degli utenti per fornire commenti e suggerimenti su come migliorare Servizi multimediali di Azure.Use the User Voice forum to provide feedback and make suggestions on how to improve Azure Media Services. È anche possibile passare direttamente a una delle categorie seguenti:You also can go directly to one of the following categories: