教程:使用媒体服务 v3 对视频进行上载、编码和流式传输Tutorial: Upload, encode, and stream videos with Media Services v3

媒体服务徽标 v3media services logo v3


备注

尽管本教程使用了 .NET SDK 示例,但 REST APICLI 或其他受支持的 SDK 的常规步骤是相同的。Even though this tutorial uses .NET SDK examples, the general steps are the same for REST API, CLI, or other supported SDKs.

使用 Azure 媒体服务可以将媒体文件编码为可在各种浏览器和设备上播放的格式。Azure Media Services lets you encode your media files into formats that play on a wide variety of browsers and devices. 例如,可能需要以 Apple 的 HLS 或 MPEG DASH 格式流式传输内容。For example, you might want to stream your content in Apple's HLS or MPEG DASH formats. 在流式传输之前,应该对高质量的数字媒体文件进行编码。Before streaming, you should encode your high-quality digital media file. 有关编码的帮助,请参阅编码概念For help with encoding, see Encoding concept. 本教程上传本地视频文件并对上传的文件进行编码。This tutorial uploads a local video file and encodes the uploaded file. 还可以对可通过 HTTPS URL 访问的内容进行编码。You can also encode content that you make accessible via an HTTPS URL. 有关详细信息,请参阅从 HTTP URL 创建作业输入For more information, see Create a job input from an HTTP(s) URL.

使用 Azure Media Player 播放视频

本教程演示如何:This tutorial shows you how to:

  • 下载本主题中所述的示例应用。Download the sample app described in the topic.
  • 检查用于上传、编码和流式传输的代码。Examine the code that uploads, encodes, and streams.
  • 运行应用。Run the app.
  • 测试流式处理 URL。Test the streaming URL.
  • 清理资源。Clean up resources.

如果没有 Azure 订阅,请在开始前创建一个试用订阅If you don't have an Azure subscription, create a Trial Subscription before you begin.

先决条件Prerequisites

下载和设置示例Download and set up the sample

使用以下命令将具有流式处理 .NET 示例的 GitHub 存储库克隆到计算机:Clone a GitHub repository that has the streaming .NET sample to your machine using the following command:

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

该示例位于 UploadEncodeAndStreamFiles 文件夹。The sample is located in the UploadEncodeAndStreamFiles folder.

打开下载的项目中的 appsettings.jsonOpen appsettings.json in your downloaded project. 将值替换为在访问 API 中获取的凭据。Replace the values with credentials that you got from accessing APIs.

检查用于上传、编码和流式传输的代码Examine the code that uploads, encodes, and streams

本节讨论 UploadEncodeAndStreamFiles 项目的 Program.cs 文件中定义的函数。This section examines functions defined in the Program.cs file of the UploadEncodeAndStreamFiles project.

该示例执行以下操作:The sample performs the following actions:

  1. 创建一个新转换(首先检查指定的转换是否存在)。Creates a new Transform (first, checks if the specified Transform exists).
  2. 创建一个输出资产,用作编码作业的输出。Creates an output Asset that's used as the encoding Job's output.
  3. 创建一个输入资产并将指定的本地视频文件上传到其中。Create an input Asset and uploads the specified local video file into it. 该资产用作作业的输入。The asset is used as the job's input.
  4. 使用创建的输入和输出提交编码作业。Submits the encoding job using the input and output that was created.
  5. 检查作业的状态。Checks the job's status.
  6. 创建流定位符Creates a Streaming Locator.
  7. 生成流式处理 URL。Builds streaming URLs.

开始结合使用媒体服务 API 与 .NET SDKStart using Media Services APIs with .NET SDK

若要开始将媒体服务 API 与 .NET 结合使用,需要创建 AzureMediaServicesClient 对象。To start using Media Services APIs with .NET, you need to create an AzureMediaServicesClient object. 若要创建对象,必须提供客户端所需凭据以使用 Azure AD 连接到 Azure。To create the object, you must supply credentials needed for the client to connect to Azure using Azure AD. 在本文开头克隆的代码中,GetCredentialsAsync 函数根据本地配置文件中提供的凭据创建 ServiceClientCredentials 对象。In the code you cloned at the beginning of the article, the GetCredentialsAsync function creates the ServiceClientCredentials object based on the credentials supplied in local configuration file.

private static async Task<IAzureMediaServicesClient> CreateMediaServicesClientAsync(ConfigWrapper config)
{
    var credentials = await GetCredentialsAsync(config);

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

创建输入资产并将本地文件上传到该资产Create an input asset and upload a local file into it

CreateInputAsset 函数创建新的输入资产并将指定的本地视频文件上传到该资产。The CreateInputAsset function creates a new input Asset and uploads the specified local video file into it. 此资产用作编码作业的输入。This Asset is used as the input to your encoding job. 在媒体服务 v3 中,作业输入可以是资产,也可以是可通过 HTTPS URL 使用媒体服务帐户访问的内容。In Media Services v3, the input to a Job can either be an Asset or content that you make available to your Media Services account via HTTPS URLs. 要了解如何从 HTTPS URL 进行编码,请参阅文章。To learn how to encode from an HTTPS URL, see this article.

在媒体服务 v3 中,使用 Azure 存储 API 上传文件。In Media Services v3, you use Azure Storage APIs to upload files. 以下 .NET 片段显示如何上传。The following .NET snippet shows how.

以下函数执行以下操作:The following function performs these actions:

  • 创建资产。Creates an Asset.

  • 获取资产的存储中容器的可写 SAS URLGets a writable SAS URL to the asset's container in storage.

    如果使用资产的 ListContainerSas 函数获取 SAS URL,请注意,该函数将返回多个 SAS URL,因为每个存储帐户有两个存储帐户密钥。If using asset's ListContainerSas function to get SAS URLs, note that the function returns multiple SAS URLs as there are two storage account keys for each storage account. 存储帐户有两个密钥,因为它支持存储帐户密钥无缝轮换(例如,使用一个密钥时更改另一个,然后开始使用新密钥并轮换其他密钥)。A storage account has two keys because it allows for seamless rotation of storage account keys (for example, change one while using the other then start using the new key and rotate the other key). 第一个 SAS URL 表示存储 key1,第二个表示存储 key2。The 1st SAS URL represents storage key1 and second one storage key2.

  • 使用 SAS URL 将文件上传到存储中的容器中。Uploads the file into the container in storage using the 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.  
    CloudBlobContainer container = new CloudBlobContainer(sasUri);
    var blob = container.GetBlockBlobReference(Path.GetFileName(fileToUpload));

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

    return asset;
}

创建输出资产用于存储作业结果Create an output asset to store the result of a job

输出资产会存储作业编码的结果。The output Asset stores the result of your encoding job. 项目定义 DownloadResults 函数,该函数将结果从此输出资产中下载到输出文件夹中,便于用户查看获取的内容。The project defines the DownloadResults function that downloads the results from this output asset into the "output" folder, so you can see what you got.

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

创建转换和一个对上传的文件进行编码的作业Create a Transform and a Job that encodes the uploaded file

对媒体服务中的内容进行编码或处理时,一种常见的模式是将编码设置设为脚本。When encoding or processing content in Media Services, it's a common pattern to set up the encoding settings as a recipe. 然后,需提交作业,将该脚本应用于视频。You would then submit a Job to apply that recipe to a video. 为每个新视频提交新作业后,可将该脚本应用到库中的所有视频。By submitting new jobs for each new video, you're applying that recipe to all the videos in your library. 媒体服务中的脚本称为“转换”。A recipe in Media Services is called a Transform. 有关详细信息,请参阅转换和作业For more information, see Transforms and Jobs. 本教程中的示例定义有关将视频进行编码以将其流式传输到各种 iOS 和 Android 设备的脚本。The sample described in this tutorial defines a recipe that encodes the video in order to stream it to a variety of iOS and Android devices.

转换Transform

创建新转换实例时,需要指定希望生成的输出内容。When creating a new Transform instance, you need to specify what you want it to produce as an output. 所需参数是 TransformOutput 对象,如以下代码所示。The required parameter is a TransformOutput object, as shown in the code below. 每个 TransformOutput 包含一个预设 。Each TransformOutput contains a Preset. 预设介绍了视频和/或音频处理操作的分步说明,这些操作将用于生成所需的 TransformOutput 。Preset describes the step-by-step instructions of video and/or audio processing operations that are to be used to generate the desired TransformOutput. 本文中的示例使用名为 AdaptiveStreaming 的内置预设。The sample described in this article uses a built-in Preset called AdaptiveStreaming. 此预设将输入的视频编码为基于输入的分辨率和比特率自动生成的比特率阶梯(比特率 - 分辨率对),并通过与每个比特率 - 分辨率对相对应的 H.264 视频和 AAC 音频生成 ISO MP4 文件。The Preset encodes the input video into an auto-generated bitrate ladder (bitrate-resolution pairs) based on the input resolution and bitrate, and produces ISO MP4 files with H.264 video and AAC audio corresponding to each bitrate-resolution pair. 有关此预设的信息,请参阅自动生成比特率阶梯For information about this Preset, see auto-generating bitrate ladder.

可以使用内置 EncoderNamedPreset 或使用自定义预设。You can use a built-in EncoderNamedPreset or use custom presets. 有关详细信息,请参阅如何自定义编码器预设For more information, see How to customize encoder presets.

在创建时转换,首先应检查是否其中一个已存在使用获取方法,如下面的代码中所示。When creating a Transform, you should first check if one already exists using the Get method, as shown in the code that follows. 在 Media Services v3获取实体上的方法返回null如果实体不存在 (不区分大小写的名称检查)。In Media Services v3, Get methods on entities return null if the entity doesn’t exist (a case-insensitive check on the name).

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;
}

作业Job

如上所述,转换对象为脚本,作业则是对媒体服务的实际请求,请求将转换应用到给定输入视频或音频内容。As mentioned above, the Transform object is the recipe and a Job is the actual request to Media Services to apply that Transform to a given input video or audio content. 作业指定输入视频位置和输出位置等信息。The Job specifies information like the location of the input video, and the location for the output.

在此示例中,已从本地计算机上传输入视频。In this example, the input video has been uploaded from your local machine. 如果想要了解如何从 HTTPS URL 进行编码,请参阅文章。If you want to learn how to encode from an HTTPS URL, see this article.

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;
}

等待作业完成Wait for the Job to complete

此作业需要一些时间才能完成,完成时可发出通知。The job takes some time to complete and when it does you want to be notified. 以下代码示例显示如何轮询服务以获取作业状态。The code sample below shows how to poll the service for the status of the Job. 对于生产应用程序,由于可能出现延迟,并不建议将轮询作为最佳做法。Polling isn't a recommended best practice for production apps because of potential latency. 如果在帐户上过度使用轮询,轮询会受到限制。Polling can be throttled if overused on an account. 开发者应改用事件网格。Developers should instead use Event Grid.

事件网格旨在实现高可用性、一致性能和动态缩放。Event Grid is designed for high availability, consistent performance, and dynamic scale. 使用事件网格,应用可以侦听和响应来自几乎所有 Azure 服务和自定义源的事件。With Event Grid, your apps can listen for and react to events from virtually all Azure services, as well as custom sources. 处理基于 HTTP 的反应事件非常简单,这有助于通过对事件的智能筛选和路由生成高效的解决方案。Simple, HTTP-based reactive event handling helps you build efficient solutions through intelligent filtering and routing of events. 请参阅将事件路由到自定义 Web 终结点See Route events to a custom web endpoint.

作业通常会经历以下状态:已计划已排队正在处理已完成(最终状态)。The Job usually goes through the following states: Scheduled, Queued, Processing, Finished (the final state). 如果作业出错,则显示“错误”状态。If the job has encountered an error, you get the Error state. 如果作业正处于取消过程中,则显示“正在取消”,完成时则显示“已取消” 。If the job is in the process of being canceled, you get Canceling and Canceled when it's done.

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

    Job job = null;

    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;
}

作业错误代码Job error codes

请参阅错误代码See Error codes.

获取流定位符Get a Streaming Locator

编码完成后,下一步是使输出资产中的视频可供客户端播放。After the encoding is complete, the next step is to make the video in the output Asset available to clients for playback. 可通过两个步骤完成此操作:首先创建流式处理定位符,然后生成客户端可以使用的流式处理 URL。You can make it available in two steps: first, create a Streaming Locator, and second, build the streaming URLs that clients can use.

创建流定位符的过程称为发布。The process of creating a Streaming Locator is called publishing. 默认情况下,除非配置可选的开始和结束时间,否则调用 API 后,流式处理定位符立即生效,并持续到被删除为止。By default, the Streaming Locator is valid immediately after you make the API calls, and lasts until it's deleted, unless you configure the optional start and end times.

创建 StreamingLocator 时,需要指定所需的 StreamingPolicyName。When creating a StreamingLocator, you'll need to specify the desired StreamingPolicyName. 在此示例中将流式传输明文(或未加密的内容),因此使用预定义的明文流式传输策略 (PredefinedStreamingPolicy.ClearStreamingOnly)。In this example, you'll be streaming in-the-clear (or non-encrypted content) so the predefined clear streaming policy (PredefinedStreamingPolicy.ClearStreamingOnly) is used.

重要

使用自定义的流策略时,应为媒体服务帐户设计有限的一组此类策略,并在需要同样的加密选项和协议时重新将这些策略用于 StreamingLocators。When using a custom Streaming Policy, you should design a limited set of such policies for your Media Service account, and re-use them for your StreamingLocators whenever the same encryption options and protocols are needed. 媒体服务帐户具有对应于流式处理策略条目数的配额。Your Media Service account has a quota for the number of Streaming Policy entries. 不应为每个流式处理定位符创建新的流式处理策略。You shouldn't be creating a new Streaming Policy for each Streaming Locator.

以下代码假定使用唯一的 locatorName 调用该函数。The following code assumes that you're calling the function with a unique 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;
}

虽然本主题的示例讨论的是流式传输,但可以使用相同的调用创建一个流定位符,通过渐进式下载的方式来提供视频。While the sample in this topic discusses streaming, you can use the same call to create a Streaming Locator for delivering video via progressive download.

获取流式 URLGet streaming URLs

创建流定位符后,可以获取流 URL,如 GetStreamingURLs 中所示。Now that the Streaming Locator has been created, you can get the streaming URLs, as shown in GetStreamingURLs. 若要生成 URL,需要连接流式处理终结点的主机名和流定位符路径。To build a URL, you need to concatenate the Streaming Endpoint host name and the Streaming Locator path. 此示例使用默认的流式处理终结点In this sample, the default Streaming Endpoint is used. 首次创建媒体服务帐户时,此默认的流式处理终结点处于停止状态,因此需要调用 StartWhen you first create a Media Service account, this default Streaming Endpoint will be in a stopped state, so you need to call Start.

备注

在此方法中,需要指定在创建输出资产的流定位符时所用的 locatorName。In this method, you need the locatorName that was used when creating the Streaming Locator for the output Asset.

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();
        uriBuilder.Scheme = "https";
        uriBuilder.Host = streamingEndpoint.HostName;

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

    return streamingUrls;
}

清理媒体服务帐户中的资源Clean up resources in your Media Services account

通常情况下,除了打算重复使用的对象,用户应清理所有内容(通常将重复使用转换并保留 StreamingLocators 等)。Generally, you should clean up everything except objects that you're planning to reuse (typically, you'll reuse Transforms, and you'll persist StreamingLocators, etc.). 如果希望帐户在试验后保持干净状态,则删除不打算重复使用的资源。If you want your account to be clean after experimenting, delete the resources that you don't plan to reuse. 例如,以下代码可删除作业、已创建的资产和内容密钥策略:For example, the following code deletes the job, created assets and content key policy:

private static async Task CleanUpAsync(
    IAzureMediaServicesClient client,
    string resourceGroupName,
    string accountName,
    string transformName)
{

    var jobs = await client.Jobs.ListAsync(resourceGroupName, accountName, transformName);
    foreach (var job in jobs)
    {
        await client.Jobs.DeleteAsync(resourceGroupName, accountName, transformName, job.Name);
    }

    var assets = await client.Assets.ListAsync(resourceGroupName, accountName);
    foreach (var asset in assets)
    {
        await client.Assets.DeleteAsync(resourceGroupName, accountName, asset.Name);
    }
}

运行示例应用Run the sample app

  1. 按 Ctrl+F5 运行 EncodeAndStreamFiles 应用。Press Ctrl+F5 to run the EncodeAndStreamFiles app.
  2. 从控制台复制其中一个流式 URL。Copy one of the streaming URLs from the console.

此示例显示的 URL 可用于使用不同协议来播放视频:This example displays URLs that can be used to play back the video using different protocols:

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

测试流式 URLTest the streaming URL

本文使用 Azure Media Player 测试流式传输。To test the stream, this article uses Azure Media Player.

备注

如果播放器在 Https 站点上进行托管,请确保将 URL 更新为“https”。If a player is hosted on an https site, make sure to update the URL to "https".

  1. 打开 Web 浏览器并导航到 https://aka.ms/azuremediaplayer/Open a web browser and navigate to https://aka.ms/azuremediaplayer/.
  2. 在“URL:”框中,粘贴运行应用时获取的某个流式处理 URL 值。In the URL: box, paste one of the streaming URL values you got when you ran the app.
  3. 选择“更新播放器”。Select Update Player.

Azure Media Player 可用于测试,但不可在生产环境中使用。Azure Media Player can be used for testing but shouldn't be used in a production environment.

清理资源Clean up resources

如果不再需要资源组中的任何一个资源(包括为本教程创建的媒体服务和存储帐户),请删除之前创建的资源组。If you no longer need any of the resources in your resource group, including the Media Services and storage accounts you created for this tutorial, delete the resource group you created earlier.

执行以下 CLI 命令:Execute the following CLI command:

az group delete --name amsResourceGroup

多线程处理Multithreading

Azure 媒体服务 v3 SDK 不是线程安全的。The Azure Media Services v3 SDKs aren't thread-safe. 开发多线程应用时,应在每个线程上生成并使用一个新的 AzureMediaServicesClient 对象。When developing a multi-threaded app, you should generate and use a new AzureMediaServicesClient object per thread.