如何使用 Azure 媒体服务执行实时流式处理以通过 .NET 创建多比特率流How to perform live streaming using Azure Media Services to create multi-bitrate streams with .NET

Note

要完成本教程,需要一个 Azure 帐户。 有关详细信息,请参阅 Azure 试用

概述Overview

本教程指导完成创建 频道 的步骤,该频道接收单比特率实时流,并将其编码为多比特率流。This tutorial walks you through the steps of creating a Channel that receives a single-bitrate live stream and encodes it to multi-bitrate stream.

有关为实时编码启用的通道的更多相关概念信息,请参阅 使用 Azure 媒体服务执行实时流式处理以创建多比特率流For more conceptual information related to Channels that are enabled for live encoding, see Live streaming using Azure Media Services to create multi-bitrate streams.

常见的实时流方案Common Live Streaming Scenario

以下步骤介绍创建常见的实时流式处理应用程序时涉及的任务。The following steps describe tasks involved in creating common live streaming applications.

Note

目前,直播活动的最大建议持续时间为 8 小时。Currently, the max recommended duration of a live event is 8 hours.

  1. 将视频摄像机连接到计算机。Connect a video camera to a computer. 启动并配置可以通过以下协议之一输出单比特率流的本地实时编码器:RTMP 或平滑流式处理。Launch and configure an on-premises live encoder that can output a single bitrate stream in one of the following protocols: RTMP or Smooth Streaming. 有关详细信息,请参阅 Azure 媒体服务 RTMP 支持和实时编码器For more information, see Azure Media Services RTMP Support and Live Encoders.

    此步骤也可以在创建频道后执行。This step could also be performed after you create your Channel.

  2. 创建并启动频道。Create and start a Channel.

  3. 检索频道引入 URL。Retrieve the Channel ingest URL.

    实时编码器使用引入 URL 将流发送到频道。The ingest URL is used by the live encoder to send the stream to the Channel.

  4. 检索频道预览 URL。Retrieve the Channel preview URL.

    使用此 URL 来验证频道是否正常接收实时流。Use this URL to verify that your channel is properly receiving the live stream.

  5. 创建资源。Create an asset.

  6. 如果想让资源在播放期间进行动态加密,请执行以下操作:If you want for the asset to be dynamically encrypted during playback, do the following:

  7. 创建内容密钥。Create a content key.

  8. 配置内容密钥授权策略。Configure the content key's authorization policy.

  9. 配置资产传送策略(由动态打包和动态加密使用)。Configure asset delivery policy (used by dynamic packaging and dynamic encryption).

  10. 创建节目并指定使用创建的资产。Create a program and specify to use the asset that you created.

  11. 通过创建按需定位器发布与节目关联的资产。Publish the asset associated with the program by creating an OnDemand locator.

    Note

    创建 AMS 帐户后,会将一个处于“已停止”状态的默认流式处理终结点添加到帐户。 When your AMS account is created a default streaming endpoint is added to your account in the Stopped state. 要从中流式传输内容的流式处理终结点必须处于“正在运行”状态。 The streaming endpoint from which you want to stream content has to be in the Running state.

  12. 在准备好开始流式传输和存档时,启动节目。Start the program when you are ready to start streaming and archiving.

  13. (可选)可以向实时编码器发信号,以启动广告。Optionally, the live encoder can be signaled to start an advertisement. 将广告插入到输出流中。The advertisement is inserted in the output stream.

  14. 在要停止对事件进行流式传输和存档时,停止节目。Stop the program whenever you want to stop streaming and archiving the event.

  15. 删除节目(并选择性地删除资产)。Delete the Program (and optionally delete the asset).

学习内容What you'll learn

本文演示如何使用适用于 .NET 的媒体服务 SDK 对频道和节目执行不同操作。This article shows you how to execute different operations on channels and programs using Media Services .NET SDK. 由于许多操作都长时间运行,因此将使用管理长时间运行的操作的 .NET API。Because many operations are long-running .NET APIs that manage long running operations are used.

本文介绍如何执行以下操作:The article shows how to do the following:

  1. 创建并启动频道。Create and start a channel. 将使用长时间运行的 API。Long-running APIs are used.
  2. 获取频道引入(输入)终结点。Get the channels ingest (input) endpoint. 应将此终结点提供给可以发送单比特率实时流的编码器。This endpoint should be provided to the encoder that can send a single bitrate live stream.
  3. 获取预览终结点。Get the preview endpoint. 此终结点用于预览流。This endpoint is used to preview your stream.
  4. 创建用于存储内容的资产。Create an asset that is used to store your content. 还应配置资源传送策略,如此示例中所示。The asset delivery policies should be configured as well, as shown in this example.
  5. 创建节目并指定使用先前创建的资源。Create a program and specify to use the asset that was created earlier. 启动该节目。Start the program. 将使用长时间运行的 API。Long-running APIs are used.
  6. 为资源创建定位器,以便发布内容,并可以将内容流式传输到客户端。Create a locator for the asset, so the content gets published and can be streamed to your clients.
  7. 显示和隐藏清单。Show and hide slates. 启动和停止广告。Start and stop advertisements. 将使用长时间运行的 API。Long-running APIs are used.
  8. 清理频道及所有关联的资源。Clean up your channel and all the associated resources.

必备条件Prerequisites

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

  • 一个 Azure 帐户。An Azure account. 如果没有帐户,可以在几分钟内创建一个试用帐户。If you don't have an account, you can create a trial account in just a couple of minutes. 有关详细信息,请参阅 Azure 试用For details, see Azure Trial. 获取可用来尝试付费版 Azure 服务的信用额度。You get credits that can be used to try out paid Azure services. 即使在信用额度用完之后,也可以保留该帐户,使用免费的 Azure 服务和功能,例如 Azure 应用服务中的 Web 应用功能。Even after the credits are used up, you can keep the account and use free Azure services and features, such as the Web Apps feature in Azure App Service.
  • 一个媒体服务帐户。A Media Services account. 若要创建媒体服务帐户,请参阅创建帐户To create a Media Services account, see Create Account.
  • Visual Studio 2010 SP1(Professional、Premium、Ultimate 或 Express)或更高版本。Visual Studio 2010 SP1 (Professional, Premium, Ultimate, or Express) or later versions.
  • 必须使用适用于 .NET 的媒体服务 SDK 版本 3.2.0.0 或更高版本。You must use Media Services .NET SDK version 3.2.0.0 or newer.
  • 可以发送单比特率实时流的摄像头和编码器。A webcam and an encoder that can send a single bitrate live stream.

注意事项Considerations

  • 目前,直播活动的最大建议持续时间为 8 小时。Currently, the max recommended duration of a live event is 8 hours.
  • 不同 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 article.

下载示例Download sample

可以从此处下载本文所述示例。You can download the sample that is described in this article from here.

使用用于 .NET 的媒体服务 SDK 进行开发设置Set up for development with Media Services SDK for .NET

设置开发环境,并根据使用 .NET 进行媒体服务开发中所述,在 app.config 文件中填充连接信息。Set up your development environment and populate the app.config file with connection information, as described in Media Services development with .NET.

代码示例Code example

using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Net;
using Microsoft.WindowsAzure.MediaServices.Client;
using Microsoft.WindowsAzure.MediaServices.Client.DynamicEncryption;

namespace EncodeLiveStreamWithAmsClear
{
    class Program
    {
        private const string ChannelName = "channel001";
        private const string AssetName = "asset001";
        private const string ProgramName = "program001";

        // Read values from the App.config file.
        private static readonly string _AADTenantDomain =
            ConfigurationManager.AppSettings["AMSAADTenantDomain"];
        private static readonly string _RESTAPIEndpoint =
            ConfigurationManager.AppSettings["AMSRESTAPIEndpoint"];
        private static readonly string _AMSClientId =
            ConfigurationManager.AppSettings["AMSClientId"];
        private static readonly string _AMSClientSecret =
            ConfigurationManager.AppSettings["AMSClientSecret"];

        private static CloudMediaContext _context = null;

        static void Main(string[] args)
        {
            AzureAdTokenCredentials tokenCredentials =
                new AzureAdTokenCredentials(_AADTenantDomain,
                    new AzureAdClientSymmetricKey(_AMSClientId, _AMSClientSecret),
                    AzureEnvironments.AzureChinaCloudEnvironment);

            var tokenProvider = new AzureAdTokenProvider(tokenCredentials);

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

            IChannel channel = CreateAndStartChannel();

            // The channel's input endpoint:
            string ingestUrl = channel.Input.Endpoints.FirstOrDefault().Url.ToString();

            Console.WriteLine("Intest URL: {0}", ingestUrl);


            // Use the previewEndpoint to preview and verify 
            // that the input from the encoder is actually reaching the Channel. 
            string previewEndpoint = channel.Preview.Endpoints.FirstOrDefault().Url.ToString();

            Console.WriteLine("Preview URL: {0}", previewEndpoint);

            // When Live Encoding is enabled, you can now get a preview of the live feed as it reaches the Channel. 
            // This can be a valuable tool to check whether your live feed is actually reaching the Channel. 
            // The thumbnail is exposed via the same end-point as the Channel Preview URL.
            string thumbnailUri = new UriBuilder
            {
                Scheme = Uri.UriSchemeHttps,
                Host = channel.Preview.Endpoints.FirstOrDefault().Url.Host,
                Path = "thumbnails/input.jpg"
            }.Uri.ToString();

            Console.WriteLine("Thumbain URL: {0}", thumbnailUri);

            // Once you previewed your stream and verified that it is flowing into your Channel, 
            // you can create an event by creating an Asset, Program, and Streaming Locator. 
            IAsset asset = CreateAndConfigureAsset();

            IProgram program = CreateAndStartProgram(channel, asset);

            ILocator locator = CreateLocatorForAsset(program.Asset, program.ArchiveWindowLength);

            // You can use slates and ads only if the channel type is Standard.  
            StartStopAdsSlates(channel);

            // Once you are done streaming, clean up your resources.
            Cleanup(channel);
        }

        public static IChannel CreateAndStartChannel()
        {
            var channelInput = CreateChannelInput();
            var channelPreview = CreateChannelPreview();
            var channelEncoding = CreateChannelEncoding();

            ChannelCreationOptions options = new ChannelCreationOptions
            {
                EncodingType = ChannelEncodingType.Standard,
                Name = ChannelName,
                Input = channelInput,
                Preview = channelPreview,
                Encoding = channelEncoding
            };

            Log("Creating channel");
            IOperation channelCreateOperation = _context.Channels.SendCreateOperation(options);
            string channelId = TrackOperation(channelCreateOperation, "Channel create");

            IChannel channel = _context.Channels.FirstOrDefault(c => c.Id == channelId);

            Log("Starting channel");
            var channelStartOperation = channel.SendStartOperation();
            TrackOperation(channelStartOperation, "Channel start");

            return channel;
        }

        /// <summary>
        /// Create channel input, used in channel creation options. 
        /// </summary>
        /// <returns></returns>
        private static ChannelInput CreateChannelInput()
        {
            // When creating a Channel, you can specify allowed IP addresses in one of the following formats: 
            // IpV4 address with 4 numbers
            // CIDR address range

            return new ChannelInput
            {
                StreamingProtocol = StreamingProtocol.FragmentedMP4,
                AccessControl = new ChannelAccessControl
                {
                    IPAllowList = new List<IPRange>
                    {
                        new IPRange
                        {
                        Name = "TestChannelInput001",
                        Address = IPAddress.Parse("0.0.0.0"),
                        SubnetPrefixLength = 0
                        }
                    }
                }
            };
        }

        /// <summary>
        /// Create channel preview, used in channel creation options. 
        /// </summary>
        /// <returns></returns>
        private static ChannelPreview CreateChannelPreview()
        {
            // When creating a Channel, you can specify allowed IP addresses in one of the following formats: 
            // IpV4 address with 4 numbers
            // CIDR address range

            return new ChannelPreview
            {
                AccessControl = new ChannelAccessControl
                {
                    IPAllowList = new List<IPRange>
                {
                    new IPRange
                    {
                    Name = "TestChannelPreview001",
                    Address = IPAddress.Parse("0.0.0.0"),
                    SubnetPrefixLength = 0
                    }
                }
                }
            };
        }

        /// <summary>
        /// Create channel encoding, used in channel creation options. 
        /// </summary>
        /// <returns></returns>
        private static ChannelEncoding CreateChannelEncoding()
        {
            return new ChannelEncoding
            {
                SystemPreset = "Default720p",
                IgnoreCea708ClosedCaptions = false,
                AdMarkerSource = AdMarkerSource.Api
            };
        }

        /// <summary>
        /// Create an asset and configure asset delivery policies.
        /// </summary>
        /// <returns></returns>
        public static IAsset CreateAndConfigureAsset()
        {
            IAsset asset = _context.Assets.Create(AssetName, AssetCreationOptions.None);

            IAssetDeliveryPolicy policy =
            _context.AssetDeliveryPolicies.Create("Clear Policy",
            AssetDeliveryPolicyType.NoDynamicEncryption,
            AssetDeliveryProtocol.HLS | AssetDeliveryProtocol.SmoothStreaming | AssetDeliveryProtocol.Dash, null);

            asset.DeliveryPolicies.Add(policy);

            return asset;
        }

        /// <summary>
        /// Create a Program on the Channel. You can have multiple Programs that overlap or are sequential;
        /// however each Program must have a unique name within your Media Services account.
        /// </summary>
        /// <param name="channel"></param>
        /// <param name="asset"></param>
        /// <returns></returns>
        public static IProgram CreateAndStartProgram(IChannel channel, IAsset asset)
        {
            IProgram program = channel.Programs.Create(ProgramName, TimeSpan.FromHours(3), asset.Id);
            Log("Program created", program.Id);

            Log("Starting program");
            var programStartOperation = program.SendStartOperation();
            TrackOperation(programStartOperation, "Program start");

            return program;
        }

        /// <summary>
        /// Create locators in order to be able to publish and stream the video.
        /// </summary>
        /// <param name="asset"></param>
        /// <param name="ArchiveWindowLength"></param>
        /// <returns></returns>
        public static ILocator CreateLocatorForAsset(IAsset asset, TimeSpan ArchiveWindowLength)
        {
            // You cannot create a streaming locator using an AccessPolicy that includes write or delete permissions.            
            var locator = _context.Locators.CreateLocator
            (
                LocatorType.OnDemandOrigin,
                asset,
                _context.AccessPolicies.Create
                (
                    "Live Stream Policy",
                    ArchiveWindowLength,
                    AccessPermissions.Read
                )
            );

            return locator;
        }

        /// <summary>
        /// Perform operations on slates.
        /// </summary>
        /// <param name="channel"></param>
        public static void StartStopAdsSlates(IChannel channel)
        {
            int cueId = new Random().Next(int.MaxValue);
            var path = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"..\\..\\SlateJPG\\DefaultAzurePortalSlate.jpg"));

            Log("Creating asset");
            var slateAsset = _context.Assets.Create("Slate test asset " + DateTime.Now.ToString("yyyy-MM-dd HH-mm"), AssetCreationOptions.None);
            Log("Slate asset created", slateAsset.Id);

            Log("Uploading file");
            var assetFile = slateAsset.AssetFiles.Create("DefaultAzurePortalSlate.jpg");
            assetFile.Upload(path);
            assetFile.IsPrimary = true;
            assetFile.Update();

            Log("Showing slate");
            var showSlateOperation = channel.SendShowSlateOperation(TimeSpan.FromMinutes(1), slateAsset.Id);
            TrackOperation(showSlateOperation, "Show slate");

            Log("Hiding slate");
            var hideSlateOperation = channel.SendHideSlateOperation();
            TrackOperation(hideSlateOperation, "Hide slate");

            Log("Starting ad");
            var startAdOperation = channel.SendStartAdvertisementOperation(TimeSpan.FromMinutes(1), cueId, false);
            TrackOperation(startAdOperation, "Start ad");

            Log("Ending ad");
            var endAdOperation = channel.SendEndAdvertisementOperation(cueId);
            TrackOperation(endAdOperation, "End ad");

            Log("Deleting slate asset");
            slateAsset.Delete();
        }

        /// <summary>
        /// Clean up resources associated with the channel.
        /// </summary>
        /// <param name="channel"></param>
        public static void Cleanup(IChannel channel)
        {
            IAsset asset;
            if (channel != null)
            {
                foreach (var program in channel.Programs)
                {
                    asset = _context.Assets.FirstOrDefault(se => se.Id == program.AssetId);

                    Log("Stopping program");
                    var programStopOperation = program.SendStopOperation();
                    TrackOperation(programStopOperation, "Program stop");

                    program.Delete();

                    if (asset != null)
                    {
                        Log("Deleting locators");
                        foreach (var l in asset.Locators)
                            l.Delete();

                        Log("Deleting asset");
                        asset.Delete();
                    }
                }

                Log("Stopping channel");
                var channelStopOperation = channel.SendStopOperation();
                TrackOperation(channelStopOperation, "Channel stop");

                Log("Deleting channel");
                var channelDeleteOperation = channel.SendDeleteOperation();
                TrackOperation(channelDeleteOperation, "Channel delete");
            }
        }

        /// <summary>
        /// Track long running operations.
        /// </summary>
        /// <param name="operation"></param>
        /// <param name="description"></param>
        /// <returns></returns>
        public static string TrackOperation(IOperation operation, string description)
        {
            string entityId = null;
            bool isCompleted = false;

            Log("starting to track ", null, operation.Id);
            while (isCompleted == false)
            {
                operation = _context.Operations.GetOperation(operation.Id);
                isCompleted = IsCompleted(operation, out entityId);
                System.Threading.Thread.Sleep(TimeSpan.FromSeconds(30));
            }
            // If we got here, the operation succeeded.
            Log(description + " in completed", operation.TargetEntityId, operation.Id);

            return entityId;
        }

        /// <summary> 
        /// Checks if the operation has been completed. 
        /// If the operation succeeded, the created entity Id is returned in the out parameter.
        /// </summary> 
        /// <param name="operationId">The operation Id.</param> 
        /// <param name="channel">
        /// If the operation succeeded, 
        /// the entity Id associated with the successful operation is returned in the out parameter.</param>
        /// <returns>Returns false if the operation is still in progress; otherwise, true.</returns> 
        private static bool IsCompleted(IOperation operation, out string entityId)
        {
            bool completed = false;

            entityId = null;

            switch (operation.State)
            {
                case OperationState.Failed:
                    // Handle the failure. 
                    // For example, throw an exception. 
                    // Use the following information in the exception: operationId, operation.ErrorMessage.
                    Log("operation failed", operation.TargetEntityId, operation.Id);
                    break;
                case OperationState.Succeeded:
                    completed = true;
                    entityId = operation.TargetEntityId;
                    break;
                case OperationState.InProgress:
                    completed = false;
                    Log("operation in progress", operation.TargetEntityId, operation.Id);
                    break;
            }
            return completed;
        }

        private static void Log(string action, string entityId = null, string operationId = null)
        {
            Console.WriteLine(
            "{0,-21}{1,-51}{2,-51}{3,-51}",
            DateTime.Now.ToString("yyyy'-'MM'-'dd HH':'mm':'ss"),
            action,
            entityId ?? string.Empty,
            operationId ?? string.Empty);
        }
    }
}

后续步骤Next step

查看媒体服务学习路径。Review Media Services learning paths.

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

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

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