微软云平台媒体服务实践系列 2- 使用动态封装为iOS, Android , Windows 等多平台提供视频点播(VoD)方案

文章微软云平台媒体服务实践系列 1- 使用静态封装为iOS, Android 设备实现点播(VoD)方案  介绍了如何针对少数iOS, Android 客户端的场景,出于节约成本的目的使用媒体服务的静态封装实现视频点播方案。实际应用中,所支持的客户端越多,那么所获得的用户越多,客户端多种多样:PC端, Xbox, iOS, Android, Windows 等移动设备端,针对如此之大的客户端市场,能否搭建一套全面的点播方案呢?答案是肯定的,本文将基于微软云平台媒体服务介绍如何实现跨多移动客户端的视频点播方案。

微软的云平台媒体服务为流媒体服务提供了多种选择,在使用流媒体服务为企业做流媒体方案时,首先需要确认要流媒体接收目标,如针对广大iOS, Android移动设备,由于它们都支持HLS 格式的流媒体,微软平台支持smooth streaming 格式,基于该认知,比较推荐的是使用动态封装,但是必须额外添加流式处理单元,方法是在Azure 门户,点击媒体服务,然后点击所使用的媒体服务,选择流式处理端点标签页,选择需要编辑的流媒体端点,然后再缩放标签页下设置,如下截图所示:

     

 使用动态封装制定支持Windows, iOS, Android 平台的视频点播方案的流程如下截图所示:原始媒体文件——>转码为MP4文件——>使用动态封装功能,按照客户端需求提供流媒体码流, 构造流媒体地址——>测试。

 

 此外我们还可以在编码过程中为视频创建缩略图, 需要使用到相应的配置文件Thumbnail_Configuration.xml,内容如下:

<?xml version="1.0" encoding="utf-8"?>

<Thumbnail Size="100%,*" Type="Jpeg" Filename="{OriginalFilename}_{Size}_{ThumbnailTime}_{ThumbnailIndex}_{Date}_{Time}.{DefaultExtension}">

    <Time Value="10%" Step="10%" Stop="95%"/>

</Thumbnail>

 

整个代码流程可以参考如下:(使用流媒体服务.Net SDK,注意指定媒体服务的账号名字(accName)及key(accKey))

class Program

    {

        private static string accName = "[MediaService_accountName]";

        private static string accKey = "[MediaService_accountKey]";      

 

        private static readonly string _supportFiles =

            Path.GetFullPath(@"../..\..\..\..\");

 

        // Paths to support files (within the above base path).

        // provide paths to your own media files below to run.

        private static readonly string singleInputFilePath =

            Path.GetFullPath(_supportFiles + @"\azure.wmv");

        private static readonly string outputPath = _supportFiles;

        private static CloudMediaContext context;

        private static readonly string configFilePath = Path.GetFullPath(_supportFiles + @"\Thumbnail_Configuration.xml");

        static void Main(string[] args)

        {

            context = new CloudMediaContext(accName, accKey);

 

            string inputAssetId = CreateAssetAndUploadFile(context);

 

            //thumbnail

            IJob jobThumb = ThumbnailMaker(context, inputAssetId);

            string thumbnailUri = GetThumbnailUrl(context, jobThumb.OutputMediaAssets[0]);

 

            //encode to MP4

            IJob job = EncodeToMp4(context, inputAssetId);

            IAsset mp4Output = job.OutputMediaAssets.FirstOrDefault();                     

           

            //Dynamic Packaging

            var mp4Asset = job.OutputMediaAssets.FirstOrDefault();

            string mp4Url = GetDynamicStreamingUrl(context, mp4Asset.Id, LocatorType.Sas);

            string smoothStreamingUrl = GetDynamicStreamingUrl(context, mp4Asset.Id, LocatorType.OnDemandOrigin);

 

            string hlsStreamingUrl = smoothStreamingUrl + "(format=m3u8-aapl)";

            string mpegdashStreamingUrl = smoothStreamingUrl + "(format=mpd-time-csf)";

 

            //Output

            string content = "\n Thumbnail Url: \n" + thumbnailUri

                              +"\n Mp4 Url: \n" + mp4Url

                              + "\n Dynamic Packaging Smooth Url: \n" + smoothStreamingUrl

                              +"\n Dynamic Packaging HLS Url: \n" + hlsStreamingUrl

                              +"\n Dynamic Packaging MPEG DASH Url: \n" + mpegdashStreamingUrl;

 

            Console.WriteLine("\n Thumbnail Url: \n" + thumbnailUri);

            Console.WriteLine("\n Mp4 Url: \n" + mp4Url);

            Console.WriteLine("\n Smooth Url: \n" + smoothStreamingUrl);

            Console.WriteLine("\n HLS Url: \n" + hlsStreamingUrl);

            Console.WriteLine("\n MPEG DASH Url: \n" + mpegdashStreamingUrl);

 

            string outFilePath = Path.GetFullPath(outputPath + @"\" + "StreamingUrl.txt");

            WriteToFile(outFilePath, content);

            Console.ReadKey();

            Console.ReadKey();

        }

 

        private static string CreateAssetAndUploadFile(CloudMediaContext context)

        {

            var assetName = Path.GetFileNameWithoutExtension(singleInputFilePath);

            var inputAsset = context.Assets.Create(assetName, AssetCreationOptions.None);

            var assetFile = inputAsset.AssetFiles.Create(Path.GetFileName(singleInputFilePath));

            assetFile.UploadProgressChanged += new EventHandler<UploadProgressChangedEventArgs>(assetFile_UploadProgressChanged);          

            assetFile.Upload(singleInputFilePath);

            return inputAsset.Id;

        }

 

        static void assetFile_UploadProgressChanged(object sender, UploadProgressChangedEventArgs e)

        {

            Console.WriteLine(string.Format("{0} Progress: {1:0} Time: {2}", ((IAssetFile)sender).Name, e.Progress, DateTime.UtcNow.ToString(@"yyyy_M_d__hh_mm_ss")));

        }

 

        private static IJob EncodeToMp4(CloudMediaContext context, string inputAssetId)

        {

            var inputAsset = context.Assets.Where(a => a.Id == inputAssetId).FirstOrDefault();

            if (inputAsset == null)

                throw new ArgumentException("Could not find assetId: " + inputAssetId);         

            var encodingPreset = "H264 Adaptive Bitrate MP4 Set 720p";          

            IJob job = context.Jobs.Create(".Net Encoding " + inputAsset.Name + " to MP4 job");

            IMediaProcessor latestWameMediaProcessor = (from p in context.MediaProcessors where p.Name == "Windows Azure Media Encoder" select p).ToList()

                                                                         .OrderBy(wame => new Version(wame.Version)).LastOrDefault();

        

            ITask encodeTask = job.Tasks.AddNew(".Net Encoding to Mp4 Task ", latestWameMediaProcessor, encodingPreset, TaskOptions.None);

            encodeTask.InputAssets.Add(inputAsset);

            encodeTask.OutputAssets.AddNew(inputAsset.Name + " as " + " mp4 output asset", AssetCreationOptions.None);

            job.StateChanged += new EventHandler<JobStateChangedEventArgs>(JobStateChanged);

            job.Submit();

            job.GetExecutionProgressTask(CancellationToken.None).Wait();

            return job;

        }

 

 

       

        static void JobStateChanged(object sender, JobStateChangedEventArgs e)

        {

            Console.WriteLine(string.Format("{0}\n State: {1}\n Time: {2}\n\n",

                                           ((IJob)sender).Name, e.CurrentState, DateTime.UtcNow.ToString(@"yyyy_M_d__hh_mm_ss")));

        }

        static void WriteToFile(string outFilePath, string fileContent)

        {

            StreamWriter sr = File.CreateText(outFilePath);

            sr.Write(fileContent);

            sr.Close();

        }

 

 

        private static string GetDynamicStreamingUrl(CloudMediaContext context, string outputAssetId, LocatorType type)

        {

            var daysForWhichStreamingUrlIsActive = 365;

            var outputAsset = context.Assets.Where(a => a.Id == outputAssetId).FirstOrDefault();

            var accessPolicy = context.AccessPolicies.Create(outputAsset.Name,

                                                             TimeSpan.FromDays(daysForWhichStreamingUrlIsActive),

                                                             AccessPermissions.Read | AccessPermissions.List);

 

            var assetFiles = outputAsset.AssetFiles.ToList();

            if (type == LocatorType.OnDemandOrigin)

            {

                var assetFile = assetFiles.Where(f => f.Name.ToLower().EndsWith(".ism")).FirstOrDefault();

                if (assetFile != null)

                {

                    var locator = context.Locators.CreateLocator(LocatorType.OnDemandOrigin, outputAsset, accessPolicy);

                    Uri smoothUri = new Uri(locator.Path + assetFile.Name + "/manifest");

                    return smoothUri.ToString();

                }

            }

            if (type == LocatorType.Sas)

            {

                var mp4Files = assetFiles.Where(f => f.Name.ToLower().EndsWith(".mp4")).ToList();

                var assetFile = mp4Files.OrderBy(f => f.ContentFileSize).LastOrDefault(); //Get Largest File

                if (assetFile != null)

                {

                    var locator = context.Locators.CreateLocator(LocatorType.Sas, outputAsset, accessPolicy);

                    var mp4Uri = new UriBuilder(locator.Path);

                    mp4Uri.Path += "/" + assetFile.Name;

                    return mp4Uri.ToString();

                }

            }

            return string.Empty;

        }

                    

    

        private static IJob ThumbnailMaker(CloudMediaContext context, string inputAssetId)

        {

 

            var inputAsset = context.Assets.Where(a => a.Id == inputAssetId).FirstOrDefault();

            if (inputAsset == null)

                throw new ArgumentException("Could not find assetId: " + inputAssetId);

         

            IMediaProcessor latestWameMediaProcessor = (from p in context.MediaProcessors where p.Name == "Windows Azure Media Encoder" select p).ToList()

                                                                         .OrderBy(wame => new Version(wame.Version)).LastOrDefault();

 

            //thumbnail

            //Get thumbnail configuration from a XML file

            var ThumbnailConfig = File.ReadAllText(configFilePath);

            // Create a job

            IJob jobThumbnail = context.Jobs.Create("Thumbnail job Thumbnail Making " + inputAsset.Name);

            //Create a task

            ITask thumbnailTask = jobThumbnail.Tasks.AddNew("Thumbnail task",

                latestWameMediaProcessor,             

                ThumbnailConfig,

                TaskOptions.None);

 

            thumbnailTask.InputAssets.Add(inputAsset);

            thumbnailTask.OutputAssets.AddNew(inputAsset.Name + " as Thumbnail job", AssetCreationOptions.None);

            jobThumbnail.StateChanged += new EventHandler<JobStateChangedEventArgs>(JobStateChanged);

            jobThumbnail.Submit();

            jobThumbnail.GetExecutionProgressTask(CancellationToken.None).Wait();

            return jobThumbnail;

        }

        private static string GetThumbnailUrl(CloudMediaContext context, IAsset outputAsset)

        {           

            var output = outputAsset;

            var accessPolicy = context.AccessPolicies.Create("Read Policy", TimeSpan.FromDays(10), AccessPermissions.Read);         

            var locator = context.Locators.CreateSasLocator(output, accessPolicy);

            var primaryFile = (from a in output.AssetFiles

                               where a.IsPrimary == true

                               select a).FirstOrDefault();

            return locator.BaseUri + '/' + primaryFile.Name + locator.ContentAccessComponent;

        }

    }

最终我们可以获得编码后的视频缩略图地址、MP4 文件地址(可以直接用于网页,使用Video tag)、Smooth Streaming, HLS, MPEG-DASH地址:

Thumbnail URL: https://myappsstorage.blob.core.windows.net/asset-e157385d-1500-80c2-af19-f1e4cbb3271f/azure_1280x720_00.00.00.6346000_1_3-16-2015_8.06%20AM.jpg?sv=2012-02-12&sr=c&si=804d04b5-3450-4d00-9ed8-8d16913c2330&sig=n7chFUFuHRA3R9RMpknxvFBkCbsG2kDgx0VWd6AJfiE%3D&se=2015-03-26T08%3A06%3A18Z

MP4 URL: https://myappsstorage.blob.core.windows.net:443/asset-e157385d-1500-80c2-9cfd-f1e4cbb3517b/azure_H264_3400kbps_AAC_und_ch2_96kbps.mp4?sv=2012-02-12&sr=c&si=150c9ab8-a78c-4557-aab8-1deba001a998&sig=pcl58cJgJ8lxRPSic4tM0us5wMSON8MIEUPDZBsXa3Y%3D&se=2016-03-15T08%3A07%3A30Z

Smooth Streaming URL:   https://test20140404.origin.mediaservices.windows.net/e5b3f1e4-a662-41da-b8af-4423aaea492b/azure.ism/manifest

HLS:https://test20140404.origin.mediaservices.windows.net/e5b3f1e4-a662-41da-b8af-4423aaea492b/azure.ism/manifest(format=m3u8-aapl)

MPEG-DASH: https://test20140404.origin.mediaservices.windows.net/e5b3f1e4-a662-41da-b8af-4423aaea492b/azure.ism/manifest(format=mpd-time-csf)

 

自适应流媒体的最大的特征是根据客户端的网络带宽情况自适应地选择对应码流媒体内容进行传输,保证流畅的视频播放体验,因此需要特殊的客户端播放器。对于这3种常用的流媒体格式,可以使用如下播放器进行测试:

Smooth Streaming player: https://smf.cloudapp.net/healthmonitor

HLS: JW player: https://www.jwplayer.com/partners/azure/

MPEG-DASH player: https://dashplayer.azurewebsites.net/

 

更多关于微软云平台的媒体服务的使用实践内容,将在后续文章中介绍,敬请期待!