使用 Azure Webhook 通过 .NET 监视媒体服务作业通知Use Azure Webhooks to monitor Media Services job notifications with .NET

媒体服务徽标media services logo


备注

不会向媒体服务 v2 添加任何新特性或新功能。No new features or functionality are being added to Media Services v2.
查看最新版本:媒体服务 v3Check out the latest version, Media Services v3. 另请参阅从 v2 到 v3 的迁移指南Also, see migration guidance from v2 to v3

运行作业时,通常需要采用某种方式来跟踪作业进度。When you run jobs, you often require a way to track job progress. 可以使用 Azure Webhook 或 Azure 队列存储监视媒体服务作业通知。You can monitor Media Services job notifications by using Azure Webhooks or Azure Queue storage. 本文介绍如何使用 Webhook。This article shows how to work with webhooks.

本文介绍如何This article shows how to

  • 定义为响应 webhook 而自定义的 Azure 函数。Define an Azure Function that is customized to respond to webhooks.

    在此例中,webhook 由媒体服务在编码作业更改状态时触发。In this case, the webhook is triggered by Media Services when your encoding job changes status. 函数侦听来自媒体服务通知的 webhook 回调,并在作业完成之后发布输出资产。The function listens for the webhook call back from Media Services notifications and publishes the output asset once the job finishes.

    提示

    在继续之前,请确保了解 Azure Functions HTTP 和 webhook 绑定的工作原理。Before continuing, make sure you understand how Azure Functions HTTP and webhook bindings work.

  • 向编码任务添加 webhook,并指定此 webhook 响应的 webhook URL 和密钥。Add a webhook to your encoding task and specify the webhook URL and secret key that this webhook responds to. 本文末尾包含一个示例,该示例演示将 Webhook 添加到编码任务。You will find an example that adds a webhook to your encoding task at the end of the article.

可在此处找到各种媒体服务 .NET Azure Functions 的定义(包括本文中所示的定义)。You can find definitions of various Media Services .NET Azure Functions (including the one shown in this article) here.

必备条件Prerequisites

以下是完成本教程所需具备的条件:The following are required to complete the tutorial:

创建函数应用Create a function app

  1. 转到 Azure 门户,然后使用 Azure 帐户登录。Go to the Azure portal and sign-in with your Azure account.
  2. 根据此文中所述创建 Function App。Create a function app as described here.

配置 Function App 设置Configure function app settings

开发媒体服务函数时,可随时添加要在整个函数中使用的环境变量。When developing Media Services functions, it is handy to add environment variables that will be used throughout your functions. 若要配置应用设置,请单击“配置应用设置”链接。To configure app settings, click the Configure App Settings link.

应用程序设置部分定义了本文中所定义的 Webhook 中使用的参数。The application settings section defines parameters that are used in the webhook defined in this article. 还向应用设置添加以下参数。Also add the following parameters to the app settings.

名称Name 定义Definition 示例Example
SigningKeySigningKey 签名密钥。A signing key. j0txf1f8msjytzvpe40nxbpxdcxtqcgxy0ntj0txf1f8msjytzvpe40nxbpxdcxtqcgxy0nt
WebHookEndpointWebHookEndpoint webhook 终结点地址。A webhook endpoint address. Webhook 函数创建后即可从“获取函数 URL”链接中复制 URL 。Once your webhook function is created, you can copy the URL from the Get function URL link. https://juliakofuncapp.chinacloudsites.cn/api/Notification_Webhook_Function?code=iN2phdrTnCxmvaKExFWOTulfnm4C71mMLIy8tzLr7Zvf6Z22HHIK5g==https://juliakofuncapp.chinacloudsites.cn/api/Notification_Webhook_Function?code=iN2phdrTnCxmvaKExFWOTulfnm4C71mMLIy8tzLr7Zvf6Z22HHIK5g==

创建函数Create a function

部署 Function App 后,可在应用服务 Azure Functions 中找到它。Once your function app is deployed, you can find it among App Services Azure Functions.

  1. 选择 Function App,然后单击“新建函数”。 Select your function app and click New Function.
  2. 选择“C#”代码和“API 和 Webhook”方案 。Select C# code and API & Webhooks scenario.
  3. 选择“通用 Webhook - C#” 。Select Generic Webhook - C#.
  4. 为 Webhook 命名,然后按“创建” 。Name your webhook and press Create.

文件Files

Azure 函数与代码文件以及本部分所述的其他文件相关联。Your Azure Function is associated with code files and other files that are described in this section. 默认情况下,函数与 function.jsonrun.csx (C#) 文件相关联。By default, a function is associated with function.json and run.csx (C#) files. 需要添加 project.json 文件。You need to add a project.json file. 本部分的余下内容介绍这些文件的定义。The rest of this section shows the definitions for these files.

files

function.jsonfunction.json

Function.json 文件定义函数绑定和其他配置设置。The function.json file defines the function bindings and other configuration settings. 运行时使用此文件确定要监视的事件,以及如何将数据传入函数执行和从函数执行返回数据。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

project.json 文件包含依赖项。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

此部分中的代码演示一个作为 Webhook 的 Azure 函数的实现。The code in this section shows an implementation of an Azure Function that is a webhook. 在此示例中,函数侦听来自媒体服务通知的 webhook 回调,并在作业完成之后发布输出资产。In this sample, the function listens for the webhook call back from Media Services notifications and publishes the output asset once the job finishes.

Webhook 需要签名密钥(凭据)以匹配在配置通知终结点时传递的密钥。The webhook expects a signing key (credential) to match the one you pass when you configure the notification endpoint. 签名密钥是 64 字节 Base64 编码值,用于保护来自 Azure 媒体服务的 WebHook 回调的安全。The signing key is the 64-byte Base64 encoded value that is used to protect and secure your WebHooks callbacks from Azure Media Services.

在下面的 Webhook 定义代码中,VerifyWebHookRequestSignature 方法对通知消息执行验证 。In the webhook definition code that follows, the VerifyWebHookRequestSignature method does the verification of the notification message. 此验证的用途是确保消息由 Azure 媒体服务发送并且未篡改。The purpose of this validation is to ensure that the message was sent by Azure Media Services and hasn't been tampered with. 签名对于 Azure Functions 是可选的,因为它将 Code 值作为传输层安全性 (TLS) 上的查询参数 。The signature is optional for Azure Functions as it has the Code value as a query parameter over Transport Layer Security (TLS).

备注

不同 AMS 策略的策略限制为 1,000,000 个(例如,对于定位器策略或 ContentKeyAuthorizationPolicy)。There is a limit of 1,000,000 policies for different AMS policies (for example, for Locator policy or ContentKeyAuthorizationPolicy). 如果始终使用相同的日期/访问权限,则应使用相同的策略 ID,例如,用于要长期就地保留的定位符的策略(非上传策略)。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). 有关详细信息,请参阅主题。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.AzureChinaCloudEnvironment);

                    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.ToList().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; }
}

保存并运行函数。Save and run your function.

函数输出Function output

Webhook 触发后,上述示例会生成以下输出,值会有所变化。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.chinacloudsites.cn/api/otification_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.chinacloudapi.cn/0ac98077-2b58-4db7-a8da-789a13ac6167/BigBuckBunny.ism/manifest(format=m3u8-aapl)

向编码任务添加 WebhookAdd a webhook to your encoding task

在此部分中,演示向任务添加 webhook 通知的代码。In this section, the code that adds a webhook notification to a Task is shown. 还可以添加作业级别通知,这对于具有连锁任务的作业更有用。You can also add a job level notification, which would be more useful for a job with chained tasks.

  1. 在 Visual Studio 中创建新的 C# 控制台应用程序。Create a new C# Console Application in Visual Studio. 输入“名称”、“位置”和“解决方案名称”,并单击“确定”。Enter the Name, Location, and Solution name, and then click OK.

  2. 使用 NuGet 安装 Azure 媒体服务。Use NuGet to install Azure Media Services.

  3. 使用适当的值更新 App.config 文件:Update App.config file with appropriate values:

    • Azure 媒体服务连接信息,Azure Media Services connection information,

    • 需要获取通知的 webhook URL,webhook URL that expects to get the notifications,

    • 与 webhook 需要的密钥匹配的签名密钥。the signing key that matches the key that your webhook expects. 签名密钥是 64 字节 Base64 编码值,用于保护来自 Azure 媒体服务的 Webhook 回调的安全。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.chinacloudsites.cn/api/functionname?code=ApiKey" />
                <add key="WebhookSigningKey" value="j0txf1f8msjytzvpe40nxbpxdcxtqcgxy0nt" />
            </appSettings>
      
  4. 使用以下代码更新 Program.cs 文件: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.AzureChinaCloudEnvironment);
    
                    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;
                }
                }
            }
    

后续步骤Next steps

媒体服务 v3(最新版本)Media Services v3 (latest)

查看最新版本的 Azure 媒体服务!Check out the latest version of Azure Media Services!

媒体服务 v2(旧版)Media Services v2 (legacy)