教程:使用媒体服务 v3 对视频进行上载、编码和流式传输

媒体服务徽标 v3


备注

尽管本教程使用了 .NET SDK 示例,但 REST APICLI 或其他受支持的 SDK 的常规步骤是相同的。

使用 Azure 媒体服务可以将媒体文件编码为可在各种浏览器和设备上播放的格式。 例如,可能需要以 Apple 的 HLS 或 MPEG DASH 格式流式传输内容。 在流式传输之前,应该对高质量的数字媒体文件进行编码。 有关编码的帮助,请参阅编码概念。 本教程上传本地视频文件并对上传的文件进行编码。 还可以对可通过 HTTPS URL 访问的内容进行编码。 有关详细信息,请参阅从 HTTP URL 创建作业输入

使用 Azure Media Player 播放视频

本教程演示如何:

  • 下载本主题中所述的示例应用。
  • 检查用于上传、编码和流式传输的代码。
  • 运行应用。
  • 测试流式处理 URL。
  • 清理资源。

如果没有 Azure 试用版订阅,请在开始前创建一个试用版订阅

先决条件

下载和设置示例

使用以下命令将具有流式处理 .NET 示例的 GitHub 存储库克隆到计算机:

git clone https://github.com/Azure-Samples/media-services-v3-dotnet-tutorials.git

该示例位于 UploadEncodeAndStreamFiles 文件夹。

打开下载的项目中的 appsettings.json。 将值替换为在访问 API 中获取的凭据。

检查用于上传、编码和流式传输的代码

本节讨论 UploadEncodeAndStreamFiles 项目的 Program.cs 文件中定义的函数。

该示例执行以下操作:

  1. 创建一个新 转换(首先检查指定的转换是否存在)。
  2. 创建一个输出资产,用作编码作业的输出。
  3. 创建一个输入 资产 并将指定的本地视频文件上传到其中。 该资产用作作业的输入。
  4. 使用创建的输入和输出提交编码作业。
  5. 检查作业的状态。
  6. 创建 流定位符
  7. 生成流式处理 URL。

开始结合使用媒体服务 API 与 .NET SDK

若要开始将媒体服务 API 与 .NET 结合使用,需要创建 AzureMediaServicesClient 对象。 若要创建对象,需要提供客户端凭据以使用 Azure Active Directory 连接到 Azure。 另一个选项是使用在 GetCredentialsInteractiveAuthAsync 中实现的交互式身份验证。

public static async Task<IAzureMediaServicesClient> CreateMediaServicesClientAsync(ConfigWrapper config, bool interactive = false)
{
    ServiceClientCredentials credentials;
    if (interactive)
        credentials = await GetCredentialsInteractiveAuthAsync(config);
    else
        credentials = await GetCredentialsAsync(config);

    return new AzureMediaServicesClient(config.ArmEndpoint, credentials)
    {
        SubscriptionId = config.SubscriptionId,
    };
}

在文章开头克隆的代码中,GetCredentialsAsync 函数根据本地配置文件 (appsettings.json) 中提供的凭据或通过存储库根目录中的 .env 环境变量文件创建 ServiceClientCredentials 对象 。

private static async Task<ServiceClientCredentials> GetCredentialsAsync(ConfigWrapper config)
{
    // Use ConfidentialClientApplicationBuilder.AcquireTokenForClient to get a token using a service principal with symmetric key

    var scopes = new[] { config.ArmAadAudience + "/.default" };

    var app = ConfidentialClientApplicationBuilder.Create(config.AadClientId)
        .WithClientSecret(config.AadSecret)
        .WithAuthority(AzureCloudInstance.AzureChina, config.AadTenantId)
        .Build();

    var authResult = await app.AcquireTokenForClient(scopes)
                                             .ExecuteAsync()
                                             .ConfigureAwait(false);

    return new TokenCredentials(authResult.AccessToken, TokenType);
}

在交互式身份验证的情况下,GetCredentialsInteractiveAuthAsync 函数根据交互式身份验证和本地配置文件 (appsettings.json) 中提供的连接参数或通过存储库根目录中的 .env 环境变量文件创建 ServiceClientCredentials 对象 。 在本例中,配置或环境变量文件中均不需要 AADCLIENTID 和 AADSECRET。

private static async Task<ServiceClientCredentials> GetCredentialsInteractiveAuthAsync(ConfigWrapper config)
{
    var scopes = new[] { config.ArmAadAudience + "/user_impersonation" };

    // client application of Az Cli
    string ClientApplicationId = "04b07795-8ddb-461a-bbee-02f9e1bf7b46";

    AuthenticationResult result = null;

    IPublicClientApplication app = PublicClientApplicationBuilder.Create(ClientApplicationId)
        .WithAuthority(AzureCloudInstance.AzureChina, config.AadTenantId)
        .WithRedirectUri("http://localhost")
        .Build();

    var accounts = await app.GetAccountsAsync();

    try
    {
        result = await app.AcquireTokenSilent(scopes, accounts.FirstOrDefault()).ExecuteAsync();
    }
    catch (MsalUiRequiredException ex)
    {
        try
        {
            result = await app.AcquireTokenInteractive(scopes).ExecuteAsync();
        }
        catch (MsalException maslException)
        {
            Console.Error.WriteLine($"ERROR: MSAL interactive authentication exception with code '{maslException.ErrorCode}' and message '{maslException.Message}'.");
        }
    }
    catch (MsalException maslException)
    {
        Console.Error.WriteLine($"ERROR: MSAL silent authentication exception with code '{maslException.ErrorCode}' and message '{maslException.Message}'.");
    }

    return new TokenCredentials(result.AccessToken, TokenType);
}

创建输入资产并将本地文件上传到该资产

CreateInputAsset 函数创建新的输入资产并将指定的本地视频文件上传到该资产 。 此资产用作编码作业的输入。 在媒体服务 v3 中,作业输入可以是资产,也可以是可通过 HTTPS URL 使用媒体服务帐户访问的内容。 要了解如何从 HTTPS URL 进行编码,请参阅文章。

在媒体服务 v3 中,使用 Azure 存储 API 上传文件。 以下 .NET 片段显示如何上传。

以下函数执行以下操作:

  • 创建资产。

  • 获取资产的存储中容器的可写 SAS URL

    如果使用资产的 ListContainerSas 函数获取 SAS URL,请注意,该函数将返回多个 SAS URL,因为每个存储帐户有两个存储帐户密钥。 存储帐户有两个密钥,因为它支持存储帐户密钥无缝轮换(例如,使用一个密钥时更改另一个,然后开始使用新密钥并轮换其他密钥)。 第一个 SAS URL 表示存储 key1,第二个表示存储 key2。

  • 使用 SAS URL 将文件上传到存储中的容器中。

private static async Task<Asset> CreateInputAssetAsync(
    IAzureMediaServicesClient client,
    string resourceGroupName,
    string accountName,
    string assetName,
    string fileToUpload)
{
    // In this example, we are assuming that the asset name is unique.
    //
    // If you already have an asset with the desired name, use the Assets.Get method
    // to get the existing asset. In Media Services v3, the Get method on entities returns null 
    // if the entity doesn't exist (a case-insensitive check on the name).

    // Call Media Services API to create an Asset.
    // This method creates a container in storage for the Asset.
    // The files (blobs) associated with the asset will be stored in this container.
    Asset asset = await client.Assets.CreateOrUpdateAsync(resourceGroupName, accountName, assetName, new Asset());

    // Use Media Services API to get back a response that contains
    // SAS URL for the Asset container into which to upload blobs.
    // That is where you would specify read-write permissions 
    // and the exparation time for the SAS URL.
    var response = await client.Assets.ListContainerSasAsync(
        resourceGroupName,
        accountName,
        assetName,
        permissions: AssetContainerPermission.ReadWrite,
        expiryTime: DateTime.UtcNow.AddHours(4).ToUniversalTime());

    var sasUri = new Uri(response.AssetContainerSasUrls.First());

    // Use Storage API to get a reference to the Asset container
    // that was created by calling Asset's CreateOrUpdate method.  
    BlobContainerClient container = new BlobContainerClient(sasUri);
    BlobClient blob = container.GetBlobClient(Path.GetFileName(fileToUpload));

    // Use Strorage API to upload the file into the container in storage.
    await blob.UploadAsync(fileToUpload);

    return asset;
}

创建输出资产用于存储作业结果

输出资产会存储作业编码的结果。 项目定义 DownloadResults 函数,该函数将结果从此输出资产中下载到 输出 文件夹中,便于用户查看获取的内容。

private static async Task<Asset> CreateOutputAssetAsync(IAzureMediaServicesClient client, string resourceGroupName, string accountName, string assetName)
{
    // Check if an Asset already exists
    Asset outputAsset = await client.Assets.GetAsync(resourceGroupName, accountName, assetName);
    Asset asset = new Asset();
    string outputAssetName = assetName;

    if (outputAsset != null)
    {
        // Name collision! In order to get the sample to work, let's just go ahead and create a unique asset name
        // Note that the returned Asset can have a different name than the one specified as an input parameter.
        // You may want to update this part to throw an Exception instead, and handle name collisions differently.
        string uniqueness = $"-{Guid.NewGuid():N}";
        outputAssetName += uniqueness;

        Console.WriteLine("Warning – found an existing Asset with name = " + assetName);
        Console.WriteLine("Creating an Asset with this name instead: " + outputAssetName);
    }

    return await client.Assets.CreateOrUpdateAsync(resourceGroupName, accountName, outputAssetName, asset);
}

创建转换和一个对上传的文件进行编码的作业

对媒体服务中的内容进行编码或处理时,一种常见的模式是将编码设置设为脚本。 然后,需提交 作业,将该脚本应用于视频。 为每个新视频提交新作业后,可将该脚本应用到库中的所有视频。 媒体服务中的脚本称为“转换”。 有关详细信息,请参阅转换和作业。 本教程中的示例定义有关将视频进行编码以将其流式传输到各种 iOS 和 Android 设备的脚本。

转换

创建新实例时,需要指定希望生成的输出内容转换。 所需参数是 TransformOutput 对象,如以下代码所示。 每个 TransformOutput 包含一个预设 。 预设介绍了视频和/或音频处理操作的分步说明,这些操作将用于生成所需的 TransformOutput 。 本文中的示例使用名为 AdaptiveStreaming 的内置预设。 此预设将输入的视频编码为基于输入的分辨率和比特率自动生成的比特率阶梯(比特率 - 分辨率对),并通过与每个比特率 - 分辨率对相对应的 H.264 视频和 AAC 音频生成 ISO MP4 文件。 有关此预设的信息,请参阅自动生成比特率阶梯

可以使用内置 EncoderNamedPreset 或使用自定义预设。 有关详细信息,请参阅如何自定义编码器预设

在创建时 转换,首先应检查是否其中一个已存在使用 获取 方法,如下面的代码中所示。 在 Media Services v3 获取 实体上的方法返回 null 如果实体不存在 (不区分大小写的名称检查)。

private static async Task<Transform> GetOrCreateTransformAsync(
    IAzureMediaServicesClient client,
    string resourceGroupName,
    string accountName,
    string transformName)
{
    // Does a Transform already exist with the desired name? Assume that an existing Transform with the desired name
    // also uses the same recipe or Preset for processing content.
    Transform transform = await client.Transforms.GetAsync(resourceGroupName, accountName, transformName);

    if (transform == null)
    {
        // You need to specify what you want it to produce as an output
        TransformOutput[] output = new TransformOutput[]
        {
            new TransformOutput
            {
                // The preset for the Transform is set to one of Media Services built-in sample presets.
                // You can  customize the encoding settings by changing this to use "StandardEncoderPreset" class.
                Preset = new BuiltInStandardEncoderPreset()
                {
                    // This sample uses the built-in encoding preset for Adaptive Bitrate Streaming.
                    PresetName = EncoderNamedPreset.AdaptiveStreaming
                }
            }
        };

        // Create the Transform with the output defined above
        transform = await client.Transforms.CreateOrUpdateAsync(resourceGroupName, accountName, transformName, output);
    }

    return transform;
}

作业

如上所述,转换对象为脚本,作业则是对媒体服务的实际请求,请求将转换应用到给定输入视频或音频内容。 作业指定输入视频位置和输出位置等信息。

在此示例中,已从本地计算机上传输入视频。 如果想要了解如何从 HTTPS URL 进行编码,请参阅文章。

private static async Task<Job> SubmitJobAsync(IAzureMediaServicesClient client,
    string resourceGroupName,
    string accountName,
    string transformName,
    string jobName,
    string inputAssetName,
    string outputAssetName)
{
    // Use the name of the created input asset to create the job input.
    JobInput jobInput = new JobInputAsset(assetName: inputAssetName);

    JobOutput[] jobOutputs =
    {
        new JobOutputAsset(outputAssetName),
    };

    // In this example, we are assuming that the job name is unique.
    //
    // If you already have a job with the desired name, use the Jobs.Get method
    // to get the existing job. In Media Services v3, the Get method on entities returns null 
    // if the entity doesn't exist (a case-insensitive check on the name).
    Job job = await client.Jobs.CreateAsync(
        resourceGroupName,
        accountName,
        transformName,
        jobName,
        new Job
        {
            Input = jobInput,
            Outputs = jobOutputs,
        });

    return job;
}

等待作业完成

此作业需要一些时间才能完成,完成时可发出通知。 以下代码示例显示如何轮询服务以获取作业状态。 对于生产应用程序,由于可能出现延迟,并不建议将轮询作为最佳做法。 如果在帐户上过度使用轮询,轮询会受到限制。 开发者应改用事件网格。

事件网格旨在实现高可用性、一致性能和动态缩放。 使用事件网格,应用可以侦听和响应来自几乎所有 Azure 服务和自定义源的事件。 处理基于 HTTP 的反应事件非常简单,这有助于通过对事件的智能筛选和路由生成高效的解决方案。 请参阅将事件路由到自定义 Web 终结点

作业 通常会经历以下状态:已计划已排队正在处理已完成(最终状态)。 如果作业出错,则显示“错误”状态 。 如果作业正处于取消过程中,则显示“正在取消”,完成时则显示“已取消” 。

private static async Task<Job> WaitForJobToFinishAsync(IAzureMediaServicesClient client,
    string resourceGroupName,
    string accountName,
    string transformName,
    string jobName)
{
    const int SleepIntervalMs = 20 * 1000;

    Job job;
    do
    {
        job = await client.Jobs.GetAsync(resourceGroupName, accountName, transformName, jobName);

        Console.WriteLine($"Job is '{job.State}'.");
        for (int i = 0; i < job.Outputs.Count; i++)
        {
            JobOutput output = job.Outputs[i];
            Console.Write($"\tJobOutput[{i}] is '{output.State}'.");
            if (output.State == JobState.Processing)
            {
                Console.Write($"  Progress (%): '{output.Progress}'.");
            }

            Console.WriteLine();
        }

        if (job.State != JobState.Finished && job.State != JobState.Error && job.State != JobState.Canceled)
        {
            await Task.Delay(SleepIntervalMs);
        }
    }
    while (job.State != JobState.Finished && job.State != JobState.Error && job.State != JobState.Canceled);

    return job;
}

作业错误代码

请参阅错误代码

获取流定位符

编码完成后,下一步是使输出资产中的视频可供客户端播放。 可通过两个步骤完成此操作:首先创建流式处理定位符,然后生成客户端可以使用的流式处理 URL。

创建 流定位符 的过程称为发布。 默认情况下,除非配置可选的开始和结束时间,否则调用 API 后,流式处理定位符立即生效,并持续到被删除为止。

创建 StreamingLocator 时,需要指定所需的 StreamingPolicyName。 在此示例中将流式传输明文(或未加密的内容),因此使用预定义的明文流式传输策略 (PredefinedStreamingPolicy.ClearStreamingOnly)。

重要

使用自定义的流策略时,应为媒体服务帐户设计有限的一组此类策略,并在需要同样的加密选项和协议时重新将这些策略用于 StreamingLocators。 媒体服务帐户具有对应于流式处理策略条目数的配额。 不应为每个流式处理定位符创建新的流式处理策略。

以下代码假定使用唯一的 locatorName 调用该函数。

private static async Task<StreamingLocator> CreateStreamingLocatorAsync(
    IAzureMediaServicesClient client,
    string resourceGroup,
    string accountName,
    string assetName,
    string locatorName)
{
    StreamingLocator locator = await client.StreamingLocators.CreateAsync(
        resourceGroup,
        accountName,
        locatorName,
        new StreamingLocator
        {
            AssetName = assetName,
            StreamingPolicyName = PredefinedStreamingPolicy.ClearStreamingOnly
        });

    return locator;
}

虽然本主题的示例讨论的是流式传输,但可以使用相同的调用创建一个流定位符,通过渐进式下载的方式来提供视频。

获取流式 URL

创建 流定位符后,可以获取流 URL,如 GetStreamingURLs 中所示。 若要生成 URL,需要连接 流式处理终结点的主机名和 流定位符 路径。 此示例使用默认的 流式处理终结点。 首次创建媒体服务帐户时,此默认的 流式处理终结点 处于停止状态,因此需要调用 Start

备注

在此方法中,需要指定在创建输出资产的 流定位符 时所用的 locatorName。

private static async Task<IList<string>> GetStreamingUrlsAsync(
    IAzureMediaServicesClient client,
    string resourceGroupName,
    string accountName,
    String locatorName)
{
    const string DefaultStreamingEndpointName = "default";

    IList<string> streamingUrls = new List<string>();

    StreamingEndpoint streamingEndpoint = await client.StreamingEndpoints.GetAsync(resourceGroupName, accountName, DefaultStreamingEndpointName);

    if (streamingEndpoint != null)
    {
        if (streamingEndpoint.ResourceState != StreamingEndpointResourceState.Running)
        {
            await client.StreamingEndpoints.StartAsync(resourceGroupName, accountName, DefaultStreamingEndpointName);
        }
    }

    ListPathsResponse paths = await client.StreamingLocators.ListPathsAsync(resourceGroupName, accountName, locatorName);

    foreach (StreamingPath path in paths.StreamingPaths)
    {
        UriBuilder uriBuilder = new UriBuilder
        {
            Scheme = "https",
            Host = streamingEndpoint.HostName,

            Path = path.Paths[0]
        };
        streamingUrls.Add(uriBuilder.ToString());
    }

    return streamingUrls;
}

清理媒体服务帐户中的资源

通常情况下,除了打算重复使用的对象,用户应清理所有内容(通常将重复使用转换并保留 StreamingLocators 等)。 如果希望帐户在试验后保持干净状态,则删除不打算重复使用的资源。 例如,以下代码可删除作业、已创建的资产和内容密钥策略:

private static async Task CleanUpAsync(
   IAzureMediaServicesClient client,
   string resourceGroupName,
   string accountName,
   string transformName,
   string jobName,
   List<string> assetNames,
   string contentKeyPolicyName = null
   )
{
    await client.Jobs.DeleteAsync(resourceGroupName, accountName, transformName, jobName);

    foreach (var assetName in assetNames)
    {
        await client.Assets.DeleteAsync(resourceGroupName, accountName, assetName);
    }

    if (contentKeyPolicyName != null)
    {
        client.ContentKeyPolicies.Delete(resourceGroupName, accountName, contentKeyPolicyName);
    }
}

运行示例应用

  1. 按 Ctrl+F5 运行 EncodeAndStreamFiles 应用。
  2. 从控制台复制其中一个流式 URL。

此示例显示的 URL 可用于使用不同协议来播放视频:

显示媒体服务流式处理视频的 URL 的示例输出

测试流式 URL

本文使用 Azure Media Player 测试流式传输。

备注

如果播放器在 Https 站点上进行托管,请确保将 URL 更新为“https”。

  1. 打开 Web 浏览器并导航到 https://aka.ms/azuremediaplayer/
  2. 在“URL:”框中,粘贴运行应用时获取的某个流式处理 URL 值。
  3. 选择“更新播放器”。

Azure Media Player 可用于测试,但不可在生产环境中使用。

清理资源

如果不再需要资源组中的任何一个资源(包括为本教程创建的媒体服务和存储帐户),请删除之前创建的资源组。

执行以下 CLI 命令:

az group delete --name amsResourceGroup

多线程处理

Azure 媒体服务 v3 SDK 不是线程安全的。 开发多线程应用时,应在每个线程上生成并使用一个新的 AzureMediaServicesClient 对象。

后续步骤

现已介绍如何上传、编码和流式传输视频,请参阅以下文章: